mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2024-12-25 02:29:32 +02:00
Merge pull request #6009 from mailcow/feat/pw-reset
[Web] Add a forgot password flow
This commit is contained in:
commit
cb9ca772b1
29
data/assets/templates/pw_reset_html.tpl
Normal file
29
data/assets/templates/pw_reset_html.tpl
Normal file
@ -0,0 +1,29 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Helvetica, Arial, Sans-Serif;
|
||||
}
|
||||
/* mobile devices */
|
||||
@media all and (max-width: 480px) {
|
||||
.mob {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
Hello {{username2}},<br><br>
|
||||
|
||||
Somebody requested a new password for the {{hostname}} account associated with {{username}}.<br>
|
||||
<small>Date of the password reset request: {{date}}</small><br><br>
|
||||
|
||||
You can reset your password by clicking the link below:<br>
|
||||
<a href="{{link}}">{{link}}</a><br><br>
|
||||
|
||||
The link will be valid for the next {{token_lifetime}} minutes.<br><br>
|
||||
|
||||
If you did not request a new password, please ignore this email.<br>
|
||||
</body>
|
||||
</html>
|
11
data/assets/templates/pw_reset_text.tpl
Normal file
11
data/assets/templates/pw_reset_text.tpl
Normal file
@ -0,0 +1,11 @@
|
||||
Hello {{username2}},
|
||||
|
||||
Somebody requested a new password for the {{hostname}} account associated with {{username}}.
|
||||
Date of the password reset request: {{date}}
|
||||
|
||||
You can reset your password by clicking the link below:
|
||||
{{link}}
|
||||
|
||||
The link will be valid for the next {{token_lifetime}} minutes.
|
||||
|
||||
If you did not request a new password, please ignore this email.
|
@ -107,6 +107,7 @@ $template_data = [
|
||||
'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'],
|
||||
'q_data' => quarantine('settings'),
|
||||
'qn_data' => quota_notification('get'),
|
||||
'pw_reset_data' => reset_password('get_notification'),
|
||||
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
|
||||
'rsettings' => $rsettings,
|
||||
'rspamd_regex_maps' => $rspamd_regex_maps,
|
||||
|
@ -1073,13 +1073,17 @@ function update_sogo_static_view($mailbox = null) {
|
||||
function edit_user_account($_data) {
|
||||
global $lang;
|
||||
global $pdo;
|
||||
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
|
||||
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
|
||||
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
|
||||
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$role = $_SESSION['mailcow_cc_role'];
|
||||
$password_old = $_data['user_old_pass'];
|
||||
$pw_recovery_email = $_data['pw_recovery_email'];
|
||||
|
||||
if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@ -1088,20 +1092,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");
|
||||
$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");
|
||||
$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) {
|
||||
@ -1116,8 +1124,29 @@ function edit_user_account($_data) {
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
|
||||
update_sogo_static_view();
|
||||
}
|
||||
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");
|
||||
$stmt->execute(array(
|
||||
':recovery_email' => $pw_recovery_email,
|
||||
':username' => $username
|
||||
));
|
||||
}
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
@ -2261,6 +2290,386 @@ function uuid4() {
|
||||
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
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->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");
|
||||
$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");
|
||||
$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 get_logs($application, $lines = false) {
|
||||
if ($lines === false) {
|
||||
|
@ -184,6 +184,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'msg' => 'global_filter_written'
|
||||
);
|
||||
return true;
|
||||
break;
|
||||
case 'filter':
|
||||
$sieve = new Sieve\SieveParser();
|
||||
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
|
||||
@ -1249,6 +1250,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
|
||||
$_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
|
||||
$_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
|
||||
$_data['pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
|
||||
} else {
|
||||
$_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']);
|
||||
$_data['tls_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_tls_policy']);
|
||||
@ -1264,14 +1266,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']);
|
||||
$_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']);
|
||||
$_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']);
|
||||
$_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']);
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO `user_acl`
|
||||
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
|
||||
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`)
|
||||
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`)
|
||||
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
|
||||
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
|
||||
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds, :pw_reset) ");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':spam_alias' => $_data['spam_alias'],
|
||||
@ -1287,7 +1290,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':quarantine_attachments' => $_data['quarantine_attachments'],
|
||||
':quarantine_notification' => $_data['quarantine_notification'],
|
||||
':quarantine_category' => $_data['quarantine_category'],
|
||||
':app_passwds' => $_data['app_passwds']
|
||||
':app_passwds' => $_data['app_passwds'],
|
||||
':pw_reset' => $_data['pw_reset']
|
||||
));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
@ -1576,6 +1580,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
|
||||
$attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
|
||||
$attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
|
||||
$attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
|
||||
} else {
|
||||
$_data['acl'] = (array)$_data['acl'];
|
||||
$attr['acl_spam_alias'] = 0;
|
||||
@ -2865,21 +2870,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$_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());
|
||||
(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());
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
@ -3132,31 +3138,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':address' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET
|
||||
`active` = :active,
|
||||
`name`= :name,
|
||||
`quota` = :quota_b,
|
||||
`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)
|
||||
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,
|
||||
':relayhost' => $relayhost,
|
||||
':username' => $username
|
||||
));
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET
|
||||
`active` = :active,
|
||||
`name`= :name,
|
||||
`quota` = :quota_b,
|
||||
`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)
|
||||
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
|
||||
));
|
||||
}
|
||||
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;
|
||||
@ -3263,6 +3281,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
|
||||
$attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
|
||||
$attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
|
||||
$attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
|
||||
} else {
|
||||
foreach ($is_now as $key => $value){
|
||||
$attr[$key] = $is_now[$key];
|
||||
|
@ -3,7 +3,7 @@ function init_db_schema() {
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "26022024_1433";
|
||||
$db_version = "29072024_1000";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@ -483,6 +483,7 @@ function init_db_schema() {
|
||||
"quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"pw_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@ -694,6 +695,19 @@ function init_db_schema() {
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"reset_password" => array(
|
||||
"cols" => array(
|
||||
"username" => "VARCHAR(255) NOT NULL",
|
||||
"token" => "VARCHAR(255) NOT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("token", "created")
|
||||
),
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
|
@ -10,16 +10,54 @@ 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)) {
|
||||
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
|
||||
$_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
|
||||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
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']);
|
||||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
|
||||
header("Location: /user");
|
||||
header("Location: /");
|
||||
exit;
|
||||
} else {
|
||||
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
|
||||
$_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
|
||||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
|
||||
header("Location: /user");
|
||||
}
|
||||
} 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']);
|
||||
@ -27,11 +65,13 @@ if (isset($_POST["verify_tfa_login"])) {
|
||||
}
|
||||
|
||||
if (isset($_GET["cancel_tfa_login"])) {
|
||||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
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: /");
|
||||
header("Location: /");
|
||||
}
|
||||
|
||||
if (isset($_POST["quick_release"])) {
|
||||
|
@ -210,6 +210,12 @@ $MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:';
|
||||
// Show last IMAP and POP3 logins
|
||||
$SHOW_LAST_LOGIN = true;
|
||||
|
||||
// Maximum number of password reset tokens that can be generated at once per user
|
||||
$PW_RESET_TOKEN_LIMIT = 3;
|
||||
|
||||
// Maximum time in minutes a password reset token is valid
|
||||
$PW_RESET_TOKEN_LIFETIME = 15;
|
||||
|
||||
// UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins
|
||||
// true = required
|
||||
// false = preferred
|
||||
|
@ -380,6 +380,9 @@ $(document).ready(function() {
|
||||
if (template.acl_app_passwds == 1){
|
||||
acl.push("app_passwds");
|
||||
}
|
||||
if (template.acl_pw_reset == 1){
|
||||
acl.push("pw_reset");
|
||||
}
|
||||
$('#user_acl').selectpicker('val', acl);
|
||||
|
||||
$('#rl_value').val(template.rl_value);
|
||||
|
@ -1973,7 +1973,6 @@ if (isset($_GET['query'])) {
|
||||
case "quota_notification_bcc":
|
||||
process_edit_return(quota_notification_bcc('edit', $attr));
|
||||
break;
|
||||
break;
|
||||
case "mailq":
|
||||
process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr)));
|
||||
break;
|
||||
@ -2069,6 +2068,9 @@ if (isset($_GET['query'])) {
|
||||
case "cors":
|
||||
process_edit_return(cors('edit', $attr));
|
||||
break;
|
||||
case "reset-password-notification":
|
||||
process_edit_return(reset_password('edit_notification', $attr));
|
||||
break;
|
||||
// return no route found if no case is matched
|
||||
default:
|
||||
http_response_code(404);
|
||||
|
@ -14,6 +14,7 @@
|
||||
"prohibited": "Untersagt durch Richtlinie",
|
||||
"protocol_access": "Ändern der erlaubten Protokolle",
|
||||
"pushover": "Pushover",
|
||||
"pw_reset": "Verwalten der E-Mail zur Passwortwiederherstellung erlauben",
|
||||
"quarantine": "Quarantäne-Aktionen",
|
||||
"quarantine_attachments": "Anhänge aus Quarantäne",
|
||||
"quarantine_category": "Ändern der Quarantäne-Benachrichtigungskategorie",
|
||||
@ -248,6 +249,11 @@
|
||||
"password_policy_numbers": "Muss eine Ziffer enthalten",
|
||||
"password_policy_special_chars": "Muss Sonderzeichen enthalten",
|
||||
"password_repeat": "Passwort wiederholen",
|
||||
"password_reset_info": "Wenn keine E-Mail zur Passwortwiederherstellung hinterlegt ist, kann diese Funktion nicht genutzt werden.",
|
||||
"password_reset_settings": "Einstellungen zur Passwortwiederherstellung",
|
||||
"password_reset_tmpl_html": "HTML Vorlage",
|
||||
"password_reset_tmpl_text": "Text Vorlage",
|
||||
"password_settings": "Passwort Einstellungen",
|
||||
"priority": "Gewichtung",
|
||||
"private_key": "Private Key",
|
||||
"quarantine": "Quarantäne",
|
||||
@ -287,6 +293,8 @@
|
||||
"remove_row": "Entfernen",
|
||||
"reset_default": "Zurücksetzen auf Standard",
|
||||
"reset_limit": "Hash entfernen",
|
||||
"reset_password_vars": "<code>{{link}}</code> Der generierte Passwort-Reset-Link<br><code>{{username}}</code> Die E-Mail-Adresse des Benutzers, der die Passwortzurücksetzung angefordert hat<br><code>{{username2}}</code> Die E-Mail-Adresse zur Wiederherstellung<br><code>{{date}}</code> Das Datum, an dem die Passwort-Reset-Anfrage gestellt wurde<br><code>{{token_lifetime}}</code> Die Gültigkeitsdauer des Tokens in Minuten<br><code>{{hostname}}</code> Der mailcow Hostname",
|
||||
"restore_template": "Leer lassen, um Standard-Template wiederherzustellen.",
|
||||
"routing": "Routing",
|
||||
"rsetting_add_rule": "Regel hinzufügen",
|
||||
"rsetting_content": "Regelinhalt",
|
||||
@ -407,6 +415,7 @@
|
||||
"invalid_nexthop_authenticated": "Dieser Next Hop existiert bereits mit abweichenden Authentifizierungsdaten. Die bestehenden Authentifizierungsdaten dieses \"Next Hops\" müssen vorab angepasst werden.",
|
||||
"invalid_recipient_map_new": "Neuer Empfänger \"%s\" ist ungültig",
|
||||
"invalid_recipient_map_old": "Originaler Empfänger \"%s\" ist ungültig",
|
||||
"invalid_reset_token": "Ungültiger Rücksetz-Token",
|
||||
"ip_list_empty": "Liste erlaubter IPs darf nicht leer sein",
|
||||
"is_alias": "%s lautet bereits eine Alias-Adresse",
|
||||
"is_alias_or_mailbox": "Eine Mailbox, ein Alias oder eine sich aus einer Alias-Domain ergebende Adresse mit dem Namen %s ist bereits vorhanden",
|
||||
@ -436,6 +445,8 @@
|
||||
"password_complexity": "Passwort entspricht nicht den Richtlinien",
|
||||
"password_empty": "Passwort darf nicht leer sein",
|
||||
"password_mismatch": "Passwort-Wiederholung stimmt nicht überein",
|
||||
"password_reset_invalid_user": "Benutzer nicht gefunden oder keine E-Mail-Adresse zur Wiederherstellung eingerichtet",
|
||||
"password_reset_na": "Die Passwortwiederherstellung ist momentan nicht verfügbar. Bitte wenden Sie sich an Ihren Administrator.",
|
||||
"policy_list_from_exists": "Ein Eintrag mit diesem Wert existiert bereits",
|
||||
"policy_list_from_invalid": "Eintrag hat ein ungültiges Format",
|
||||
"private_key_error": "Schlüsselfehler: %s",
|
||||
@ -444,10 +455,12 @@
|
||||
"pushover_token": "Pushover Token hat das falsche Format",
|
||||
"quota_not_0_not_numeric": "Speicherplatz muss numerisch und >= 0 sein",
|
||||
"recipient_map_entry_exists": "Eine Empfängerumschreibung für Objekt \"%s\" existiert bereits",
|
||||
"recovery_email_failed": "E-Mail zur Wiederherstellung konnte nicht gesendet werden. Bitte wenden Sie sich an Ihren Administrator.",
|
||||
"redis_error": "Redis Fehler: %s",
|
||||
"relayhost_invalid": "Map-Eintrag %s ist ungültig",
|
||||
"release_send_failed": "Die Nachricht konnte nicht versendet werden: %s",
|
||||
"reset_f2b_regex": "Regex-Filter konnten nicht in vorgegebener Zeit zurückgesetzt werden, bitte erneut versuchen oder die Webseite neu laden.",
|
||||
"reset_token_limit_exceeded": "Das Limit für Rücksetz-Tokens wurde überschritten. Bitte versuchen Sie es später erneut.",
|
||||
"resource_invalid": "Ressourcenname %s ist ungültig",
|
||||
"rl_timeframe": "Ratelimit-Zeitraum ist inkorrekt",
|
||||
"rspamd_ui_pw_length": "Rspamd UI-Passwort muss mindestens 6 Zeichen lang sein",
|
||||
@ -467,6 +480,7 @@
|
||||
"tls_policy_map_dest_invalid": "Ziel ist ungültig",
|
||||
"tls_policy_map_entry_exists": "Eine TLS-Richtlinie \"%s\" existiert bereits",
|
||||
"tls_policy_map_parameter_invalid": "Parameter ist ungültig",
|
||||
"to_invalid": "Empfänger darf nicht leer sein",
|
||||
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
|
||||
"transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits",
|
||||
"webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s",
|
||||
@ -638,6 +652,7 @@
|
||||
"nexthop": "Next Hop",
|
||||
"none_inherit": "Keine Auswahl / Erben",
|
||||
"password": "Passwort",
|
||||
"password_recovery_email": "E-Mail zur Passwortwiederherstellung",
|
||||
"password_repeat": "Passwort wiederholen",
|
||||
"previous": "Vorherige Seite",
|
||||
"private_comment": "Privater Kommentar",
|
||||
@ -741,12 +756,19 @@
|
||||
"session_expires": "Die Sitzung wird in etwa 15 Sekunden beendet."
|
||||
},
|
||||
"login": {
|
||||
"back_to_mailcow": "Zurück zu mailcow",
|
||||
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn Login",
|
||||
"forgot_password": "> Passwort vergessen?",
|
||||
"invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.<br>Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.",
|
||||
"login": "Anmelden",
|
||||
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
|
||||
"new_password": "Neues Passwort",
|
||||
"new_password_confirm": "Neues Passwort bestätigen",
|
||||
"other_logins": "Key Login",
|
||||
"password": "Passwort",
|
||||
"reset_password": "Passwort zurücksetzen",
|
||||
"request_reset_password": "Passwortänderung anfordern",
|
||||
"username": "Benutzername"
|
||||
},
|
||||
"mailbox": {
|
||||
@ -1065,11 +1087,13 @@
|
||||
"nginx_reloaded": "Nginx wurde neu geladen",
|
||||
"object_modified": "Änderungen an Objekt %s wurden gespeichert",
|
||||
"password_policy_saved": "Passwortrichtlinie wurde erfolgreich gespeichert",
|
||||
"password_changed_success": "Das Passwort wurde erfolgreich geändert",
|
||||
"pushover_settings_edited": "Pushover-Konfiguration gespeichert, bitte den Zugang im Anschluss verifizieren.",
|
||||
"qlearn_spam": "Nachricht-ID %s wurde als Spam gelernt und gelöscht",
|
||||
"queue_command_success": "Queue-Aufgabe erfolgreich ausgeführt",
|
||||
"recipient_map_entry_deleted": "Empfängerumschreibung mit der ID %s wurde gelöscht",
|
||||
"recipient_map_entry_saved": "Empfängerumschreibung für Objekt \"%s\" wurde gespeichert",
|
||||
"recovery_email_sent": "Wiederherstellungs-E-Mail an %s gesendet",
|
||||
"relayhost_added": "Map-Eintrag %s wurde hinzugefügt",
|
||||
"relayhost_removed": "Map-Eintrag %s wurde entfernt",
|
||||
"reset_main_logo": "Standardgrafik wurde wiederhergestellt",
|
||||
@ -1202,6 +1226,7 @@
|
||||
"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.",
|
||||
"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>]",
|
||||
@ -1211,6 +1236,7 @@
|
||||
"pushover_title": "Notification Titel",
|
||||
"pushover_vars": "Wenn kein Sender-Filter definiert ist, werden alle E-Mails berücksichtigt.<br>Die direkte Absenderprüfung und reguläre Ausdrücke werden unabhängig voneinander geprüft, sie <b>hängen nicht voneinander ab</b> und werden der Reihe nach ausgeführt. <br>Verwendbare Variablen für Titel und Text (Datenschutzrichtlinien beachten)",
|
||||
"pushover_verify": "Verbindung verifizieren",
|
||||
"pw_recovery_email": "E-Mail zur Passwortwiederherstellung",
|
||||
"q_add_header": "Junk-Ordner",
|
||||
"q_all": "Alle Kategorien",
|
||||
"q_reject": "Abgelehnt",
|
||||
|
@ -14,6 +14,7 @@
|
||||
"prohibited": "Prohibited by ACL",
|
||||
"protocol_access": "Change protocol access",
|
||||
"pushover": "Pushover",
|
||||
"pw_reset": "Allow to reset mailcow user password",
|
||||
"quarantine": "Quarantine actions",
|
||||
"quarantine_attachments": "Quarantine attachments",
|
||||
"quarantine_category": "Change quarantine notification category",
|
||||
@ -256,6 +257,11 @@
|
||||
"password_policy_numbers": "Must contain at least one number",
|
||||
"password_policy_special_chars": "Must contain special characters",
|
||||
"password_repeat": "Confirmation password (repeat)",
|
||||
"password_reset_info": "If no recovery email is provided, this function cannot be used.",
|
||||
"password_reset_settings": "Password Recovery Settings",
|
||||
"password_reset_tmpl_html": "HTML Template",
|
||||
"password_reset_tmpl_text": "Text Template",
|
||||
"password_settings": "Password Settings",
|
||||
"priority": "Priority",
|
||||
"private_key": "Private key",
|
||||
"quarantine": "Quarantine",
|
||||
@ -296,6 +302,8 @@
|
||||
"remove_row": "Remove row",
|
||||
"reset_default": "Reset to default",
|
||||
"reset_limit": "Remove hash",
|
||||
"reset_password_vars": "<code>{{link}}</code> The generated password reset link<br><code>{{username}}</code> The mailbox name of the user who requested the password reset<br><code>{{username2}}</code> The recovery mailbox name<br><code>{{date}}</code> The date the password reset request was made<br><code>{{token_lifetime}}</code> The token lifetime in minutes<br><code>{{hostname}}</code> The mailcow hostname",
|
||||
"restore_template": "Leave empty to restore default template.",
|
||||
"routing": "Routing",
|
||||
"rsetting_add_rule": "Add rule",
|
||||
"rsetting_content": "Rule content",
|
||||
@ -407,6 +415,7 @@
|
||||
"invalid_nexthop_authenticated": "Next hop exists with different credentials, please update the existing credentials for this next hop first.",
|
||||
"invalid_recipient_map_new": "Invalid new recipient specified: %s",
|
||||
"invalid_recipient_map_old": "Invalid original recipient specified: %s",
|
||||
"invalid_reset_token": "Invalid reset token",
|
||||
"ip_list_empty": "List of allowed IPs cannot be empty",
|
||||
"is_alias": "%s is already known as an alias address",
|
||||
"is_alias_or_mailbox": "%s is already known as an alias, a mailbox or an alias address expanded from an alias domain.",
|
||||
@ -436,6 +445,8 @@
|
||||
"password_complexity": "Password does not meet the policy",
|
||||
"password_empty": "Password must not be empty",
|
||||
"password_mismatch": "Confirmation password does not match",
|
||||
"password_reset_invalid_user": "Mailbox not found or no recovery email is set",
|
||||
"password_reset_na": "The password recovery is currently unavailable. Please contact your administrator.",
|
||||
"policy_list_from_exists": "A record with given name exists",
|
||||
"policy_list_from_invalid": "Record has invalid format",
|
||||
"private_key_error": "Private key error: %s",
|
||||
@ -444,10 +455,12 @@
|
||||
"pushover_token": "Pushover token has a wrong format",
|
||||
"quota_not_0_not_numeric": "Quota must be numeric and >= 0",
|
||||
"recipient_map_entry_exists": "A Recipient map entry \"%s\" exists",
|
||||
"recovery_email_failed": "Could not send a recovery email. Please contact your administrator.",
|
||||
"redis_error": "Redis error: %s",
|
||||
"relayhost_invalid": "Map entry %s is invalid",
|
||||
"release_send_failed": "Message could not be released: %s",
|
||||
"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",
|
||||
@ -470,6 +483,7 @@
|
||||
"tls_policy_map_dest_invalid": "Policy destination is invalid",
|
||||
"tls_policy_map_entry_exists": "A TLS policy map entry \"%s\" exists",
|
||||
"tls_policy_map_parameter_invalid": "Policy parameter is invalid",
|
||||
"to_invalid": "Recipient must not be empty",
|
||||
"totp_verification_failed": "TOTP verification failed",
|
||||
"transport_dest_exists": "Transport destination \"%s\" exists",
|
||||
"webauthn_verification_failed": "WebAuthn verification failed: %s",
|
||||
@ -638,6 +652,7 @@
|
||||
"none_inherit": "None / Inherit",
|
||||
"nexthop": "Next hop",
|
||||
"password": "Password",
|
||||
"password_recovery_email": "Password recovery email",
|
||||
"password_repeat": "Confirmation password (repeat)",
|
||||
"previous": "Previous page",
|
||||
"private_comment": "Private comment",
|
||||
@ -741,12 +756,19 @@
|
||||
"session_expires": "Your session will expire in about 15 seconds"
|
||||
},
|
||||
"login": {
|
||||
"back_to_mailcow": "Back to mailcow",
|
||||
"delayed": "Login was delayed by %s seconds.",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn Login",
|
||||
"forgot_password": "> Forgot Password?",
|
||||
"invalid_pass_reset_token": "The reset password token is invalid or has expired.<br>Please request a new password reset link.",
|
||||
"login": "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",
|
||||
"other_logins": "Key login",
|
||||
"password": "Password",
|
||||
"reset_password": "Reset Password",
|
||||
"request_reset_password": "Request password change",
|
||||
"username": "Username"
|
||||
},
|
||||
"mailbox": {
|
||||
@ -1072,11 +1094,13 @@
|
||||
"nginx_reloaded": "Nginx was reloaded",
|
||||
"object_modified": "Changes to object %s have been saved",
|
||||
"password_policy_saved": "Password policy was saved successfully",
|
||||
"password_changed_success": "Password was successfully changed",
|
||||
"pushover_settings_edited": "Pushover settings successfully set, please verify credentials.",
|
||||
"qlearn_spam": "Message ID %s was learned as spam and deleted",
|
||||
"queue_command_success": "Queue command completed successfully",
|
||||
"recipient_map_entry_deleted": "Recipient map ID %s has been deleted",
|
||||
"recipient_map_entry_saved": "Recipient map entry \"%s\" has been saved",
|
||||
"recovery_email_sent": "Recovery email sent to %s",
|
||||
"relayhost_added": "Map entry %s has been added",
|
||||
"relayhost_removed": "Map entry %s has been removed",
|
||||
"reset_main_logo": "Reset to default logo",
|
||||
@ -1210,6 +1234,7 @@
|
||||
"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.",
|
||||
"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>]",
|
||||
@ -1220,6 +1245,7 @@
|
||||
"pushover_sound": "Sound",
|
||||
"pushover_vars": "When no sender filter is defined, all mails will be considered.<br>Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.<br>Useable variables for text and title (please take note of data protection policies)",
|
||||
"pushover_verify": "Verify credentials",
|
||||
"pw_recovery_email": "Password recovery email",
|
||||
"q_add_header": "Junk folder",
|
||||
"q_all": "All categories",
|
||||
"q_reject": "Rejected",
|
||||
|
31
data/web/reset-password.php
Normal file
31
data/web/reset-password.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /debug');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
|
||||
|
||||
if (isset($_GET['token'])) $is_reset_token_valid = reset_password("check", $_GET['token']);
|
||||
else $is_reset_token_valid = False;
|
||||
|
||||
$template = 'reset-password.twig';
|
||||
$template_data = [
|
||||
'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'),
|
||||
'is_reset_token_valid' => $is_reset_token_valid,
|
||||
'reset_token' => $_GET['token']
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
@ -22,7 +22,7 @@
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-quarantine" aria-selected="false" aria-controls="tab-config-quarantine" role="tab" data-bs-toggle="tab">{{ lang.admin.quarantine }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-quota" aria-selected="false" aria-controls="tab-config-quota" role="tab" data-bs-toggle="tab">{{ lang.admin.quota_notifications }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-rsettings" aria-selected="false" aria-controls="tab-config-rsettings" role="tab" data-bs-toggle="tab">{{ lang.admin.rspamd_settings_map }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-password-policy" aria-selected="false" aria-controls="tab-config-password-policy" role="tab" data-bs-toggle="tab">{{ lang.admin.password_policy }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-password-settings" aria-selected="false" aria-controls="tab-config-password-settings" role="tab" data-bs-toggle="tab">{{ lang.admin.password_settings }}</button></li>
|
||||
<li><button class="dropdown-item" data-bs-target="#tab-config-customize" aria-selected="false" aria-controls="tab-config-customize" role="tab" data-bs-toggle="tab">{{ lang.admin.customize }}</button></li>
|
||||
</ul>
|
||||
</li>
|
||||
@ -51,7 +51,7 @@
|
||||
{% include 'admin/tab-config-quota.twig' %}
|
||||
{% include 'admin/tab-config-rsettings.twig' %}
|
||||
{% include 'admin/tab-config-customize.twig' %}
|
||||
{% include 'admin/tab-config-password-policy.twig' %}
|
||||
{% include 'admin/tab-config-password-settings.twig' %}
|
||||
{% include 'admin/tab-sys-mails.twig' %}
|
||||
{% include 'admin/tab-globalfilter-regex.twig' %}
|
||||
</div>
|
||||
|
@ -1,40 +0,0 @@
|
||||
<div class="tab-pane fade" id="tab-config-password-policy" role="tabpanel" aria-labelledby="tab-config-password-policy">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex fs-5">
|
||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-password-policy" data-bs-toggle="collapse" aria-controls="collapse-tab-config-password-policy">
|
||||
{{ lang.admin.password_policy }}
|
||||
</button>
|
||||
<span class="d-none d-md-block">{{ lang.admin.password_policy }}</span>
|
||||
</div>
|
||||
<div id="collapse-tab-config-password-policy" class="card-body collapse" data-bs-parent="#admin-content">
|
||||
<form class="form-horizontal" data-id="passwordpolicy" role="form" method="post">
|
||||
{% for name, value in password_complexity %}
|
||||
{% if name == 'length' %}
|
||||
<div class="row mb-4">
|
||||
<label class="control-label col-sm-3 text-sm-end" for="length">{{ lang.admin.password_length }}:</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" class="form-control" min="3" max="64" name="length" id="length" value="{{ value }}" required>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<input type="hidden" name="{{ name }}" value="0">
|
||||
<div class="row mb-2">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<label>
|
||||
<input type="checkbox" class="form-check-input" name="{{ name }}" id="{{ name }}" value="1" {% if value == 1 %}checked{% endif %}> {{ lang.admin['password_policy_'~name] }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="row mt-4 mb-2">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm d-block d-sm-inline btn-success" data-item="passwordpolicy" data-action="edit_selected" data-id="passwordpolicy" data-api-url='edit/passwordpolicy' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
102
data/web/templates/admin/tab-config-password-settings.twig
Normal file
102
data/web/templates/admin/tab-config-password-settings.twig
Normal file
@ -0,0 +1,102 @@
|
||||
<div class="tab-pane fade" id="tab-config-password-settings" role="tabpanel" aria-labelledby="tab-config-password-settings">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex fs-5">
|
||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-password-settings" data-bs-toggle="collapse" aria-controls="collapse-tab-config-password-settings">
|
||||
{{ lang.admin.password_settings }}
|
||||
</button>
|
||||
<span class="d-none d-md-block">{{ lang.admin.password_settings }}</span>
|
||||
</div>
|
||||
<div id="collapse-tab-config-password-settings" class="card-body collapse" data-bs-parent="#admin-content">
|
||||
<form class="form-horizontal" data-id="passwordpolicy" role="form" method="post">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<legend>
|
||||
{{ lang.admin.password_policy }}
|
||||
</legend>
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
{% for name, value in password_complexity %}
|
||||
{% if name == 'length' %}
|
||||
<div class="row mb-4">
|
||||
<label class="control-label col-sm-3 text-sm-end" for="length">{{ lang.admin.password_length }}:</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" class="form-control" min="3" max="64" name="length" id="length" value="{{ value }}" required>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<input type="hidden" name="{{ name }}" value="0">
|
||||
<div class="row mb-2">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<label>
|
||||
<input type="checkbox" class="form-check-input" name="{{ name }}" id="{{ name }}" value="1" {% if value == 1 %}checked{% endif %}> {{ lang.admin['password_policy_'~name] }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="row mt-4 mb-2">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm d-block d-sm-inline btn-success" data-item="passwordpolicy" data-action="edit_selected" data-id="passwordpolicy" data-api-url='edit/passwordpolicy' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="form" role="form" data-id="pw_reset_notification" method="post" style="margin-top: 50px;">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<legend>
|
||||
{{ lang.admin.password_reset_settings }}
|
||||
</legend>
|
||||
<hr />
|
||||
<small>{{ lang.admin.reset_password_vars|raw }}</small><br><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-sm-6">
|
||||
<div>
|
||||
<label for="pw_reset_from">{{ lang.admin.quota_notification_sender }}:</label>
|
||||
<input type="email" class="form-control" id="pw_reset_from" name="from" value="{{ pw_reset_data.from }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div>
|
||||
<label for="pw_reset_subject">{{ lang.admin.quota_notification_subject }}:</label>
|
||||
<input type="text" class="form-control" id="pw_reset_subject" name="subject" value="{{ pw_reset_data.subject }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12" data-bs-target="#text_template" style="cursor:pointer" unselectable="on" data-bs-toggle="collapse">
|
||||
<span class="d-block"><i style="font-size:10pt;" class="bi bi-plus-square"></i> {{ lang.admin.password_reset_tmpl_text }}</span>
|
||||
<small>{{ lang.admin.restore_template }}</small>
|
||||
</div>
|
||||
<div id="text_template" class="col-12 collapse">
|
||||
<textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code mb-2" rows="20" name="text_tmpl">{{ pw_reset_data.text_tmpl|raw }}</textarea>
|
||||
</div>
|
||||
<div class="col-12 mt-3" data-bs-target="#html_template" style="cursor:pointer" unselectable="on" data-bs-toggle="collapse">
|
||||
<span class="d-block"><i style="font-size:10pt;" class="bi bi-plus-square"></i> {{ lang.admin.password_reset_tmpl_html }}</span>
|
||||
<small>{{ lang.admin.restore_template }}</small>
|
||||
</div>
|
||||
<div id="html_template" class="col-12 collapse">
|
||||
<textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" name="html_tmpl">{{ pw_reset_data.html_tmpl|raw }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-10">
|
||||
<div>
|
||||
<br>
|
||||
<a type="button" class="btn btn-sm d-block d-sm-inline btn-success" data-action="edit_selected"
|
||||
data-item="pw_reset_notification"
|
||||
data-id="pw_reset_notification"
|
||||
data-api-url='edit/reset-password-notification'
|
||||
data-api-attr='{}'><i class="bi bi-check-lg"></i> {{ lang.user.save_changes }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -112,6 +112,7 @@
|
||||
<option value="quarantine_notification" {% if template.attributes.acl_quarantine_notification == '1' %} selected{% endif %}>{{ lang.acl["quarantine_notification"] }}</option>
|
||||
<option value="quarantine_category" {% if template.attributes.acl_quarantine_category == '1' %} selected{% endif %}>{{ lang.acl["quarantine_category"] }}</option>
|
||||
<option value="app_passwds" {% if template.attributes.acl_app_passwds == '1' %} selected{% endif %}>{{ lang.acl["app_passwds"] }}</option>
|
||||
<option value="pw_reset" {% if template.attributes.acl_pw_reset == '1' %} selected{% endif %}>{{ lang.acl["pw_reset"] }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -203,6 +203,13 @@
|
||||
<input type="password" data-pwgen-field="true" class="form-control" name="password2" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<label class="control-label col-sm-2" for="pw_recovery_email">{{ lang.edit.password_recovery_email }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" class="form-control" name="pw_recovery_email" value="{{ result.attributes.recovery_email }}">
|
||||
<small class="text-muted">{{ lang.admin.password_reset_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div data-acl="{{ acl.extend_sender_acl }}" class="row mb-4">
|
||||
<label class="control-label col-sm-2" for="extended_sender_acl">{{ lang.edit.extended_sender_acl }}</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -63,6 +63,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-3 mb-4">
|
||||
<a href="/reset-password">{{ lang.login.forgot_password }}</a>
|
||||
</div>
|
||||
{% if login_delay %}
|
||||
<p><div class="my-4 alert alert-info">{{ lang.login.delayed|format(login_delay) }}</b></div></p>
|
||||
{% endif %}
|
||||
|
@ -149,6 +149,7 @@
|
||||
<option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option>
|
||||
<option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option>
|
||||
<option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option>
|
||||
<option value="pw_reset" selected>{{ lang.acl["pw_reset"] }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -318,6 +319,7 @@
|
||||
<option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option>
|
||||
<option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option>
|
||||
<option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option>
|
||||
<option value="pw_reset" selected>{{ lang.acl["pw_reset"] }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -309,6 +309,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- pw change modal -->
|
||||
<!-- pw recovery email modal -->
|
||||
<div class="modal fade" id="pwRecoveryEmailModal" tabindex="-1" role="dialog" aria-labelledby="pwRecoveryEmailModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{ lang.user.pw_recovery_email }}</h3>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" data-cached-form="false" data-id="pw_recovery_change" role="form" method="post" autocomplete="off">
|
||||
<div class="row mb-4">
|
||||
<label class="control-label col-sm-3" for="pw_recovery_email">{{ lang.user.email }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="email" class="form-control" name="pw_recovery_email" value="{{ mailboxdata.attributes.recovery_email }}">
|
||||
<small class="text-muted">{{ lang.user.password_reset_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="pw_recovery_change" data-item="null" data-api-url='edit/self' data-api-attr='{}' href="#">{{ lang.user.save }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- pw recovery email modal -->
|
||||
<!-- temp alias modal -->
|
||||
<div class="modal fade" id="tempAliasModal" tabindex="-1" role="dialog" aria-labelledby="tempAliasModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
|
57
data/web/templates/reset-password.twig
Normal file
57
data/web/templates/reset-password.twig
Normal file
@ -0,0 +1,57 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block navbar %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4" style="margin-top: 60px">
|
||||
<div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<i class="bi bi-person-fill me-2"></i> {{ lang.login.reset_password }}
|
||||
<div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
|
||||
<label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
|
||||
<input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mailcow-logo mb-4">
|
||||
<img class="main-logo" src="{{ logo|default('/img/cow_mailcow.svg') }}" alt="mailcow">
|
||||
<img class="main-logo-dark" src="{{ logo_dark|default('/img/cow_mailcow.svg') }}" alt="mailcow-logo-dark">
|
||||
</div>
|
||||
<legend>{{ ui_texts.main_name|raw }}</legend><hr />
|
||||
|
||||
{% if is_reset_token_valid %}
|
||||
<form method="post" autofill="off">
|
||||
<input type="hidden" name="token" value="{{ reset_token }}" />
|
||||
<input type="password" autocorrect="off" autocapitalize="none" class="form-control mb-2" name="new_password" placeholder="{{ lang.login.new_password }}" />
|
||||
<input type="password" autocorrect="off" autocapitalize="none" class="form-control mb-2" name="new_password2" placeholder="{{ lang.login.new_password_confirm }}" />
|
||||
|
||||
<small id="mismatch_alert" class="text-danger d-none">{{ lang.login.password_mismatch }}</small>
|
||||
<div class="d-flex justify-content-end mt-4" style="position: relative">
|
||||
<button type="submit" class="btn btn-xs-lg d-block d-sm-inline btn-success" name="pw_reset">{{ lang.login.reset_password }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% elseif reset_token is null %}
|
||||
<form method="post" autofill="off">
|
||||
<input type="text" autocorrect="off" autocapitalize="none" class="form-control mb-2" name="username" placeholder="{{ lang.login.username }}" />
|
||||
|
||||
<div class="d-flex justify-content-end mt-4" style="position: relative">
|
||||
<button type="submit" class="btn btn-xs-lg d-block d-sm-inline btn-success" name="pw_reset_request">{{ lang.login.request_reset_password }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="text-center">{{ lang.login.invalid_pass_reset_token|raw }}</p>
|
||||
<a href="/">{{ lang.login.back_to_mailcow }}</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type='text/javascript'>
|
||||
var csrf_token = '{{ csrf_token }}';
|
||||
var mailcow_cc_username = '{{ mailcow_cc_username }}';
|
||||
</script>
|
||||
{% endblock %}
|
@ -50,6 +50,7 @@
|
||||
<p>{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}∞{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}<br>{{ mailboxdata.messages }} {{ lang.user.messages }}</p>
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user