This commit is contained in:
alf 2022-01-09 15:50:54 +03:00
parent f08ed3a2ba
commit 299faf5402
19 changed files with 1422 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
config.inc.php

View File

@ -1,2 +1,16 @@
#### Password Recovery Plugin for Roundcube #### Password Recovery Plugin for Roundcube
Plugin that adds functionality so that a user can create a new password if the original is lost.
Plugin that adds functionality so that a user can
create a new password if the original is lost.
To restore the password, the user is asked a secret question,
and/or a confirmation code is sent to an additional email address
and SMS to the phone.
It is recommended that you use the "SMSTools" package to send SMS.
When checking and saving a new password,
the password is encrypted using the MD5-Crypt method.
The password is written directly to the Postfix database (mailbox table).
The Password plugin can also be used when configured accordingly.

54
bin/sendsms.sh Executable file
View File

@ -0,0 +1,54 @@
#!/bin/bash
# only for Russia!!!
COUNTRY_CODE="7"
USER="smsd"
GROUP="smsd"
SPOOLDIR="/var/spool/sms/outgoing/"
if [ -z "$*" ]; then
echo "Usage: ./sendsms.sh \"phone number\" \"message\""
exit -1
fi
DST=$1
MSG=$2
if [[ -z "${DST}" ]]; then
echo "No destination phone number"
exit -1
fi
if [[ -z "${MSG}" ]]; then
echo "No message"
exit -1
fi
if [[ $DST == +* ]]; then
DST=${DST:1:11}
fi
if [[ ${#DST} == 10 ]]; then
DST="$COUNTRY_CODE$DST"
elif [[ ${#DST} == 11 && $DST == 8* ]]; then
DST="$COUNTRY_CODE${DST:1:10}"
fi
if [[ ${#DST} != 11 ]]; then
echo "Error in destination phone number"
exit -1
fi
SMS=$(mktemp /tmp/sms_XXXXXXX)
chown :${GROUP} ${SMS}
chmod 0666 ${SMS}
echo "To: ${DST}" >> $SMS
echo "" >> $SMS
echo $MSG >> $SMS
mv ${SMS} ${SPOOLDIR}
echo 1
exit 1

24
composer.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "AlfnRU/password_recovery",
"type": "roundcube-plugin",
"description": "Plugin that adds functionality so that a user can create a new password if the original is lost.",
"homepage": "https://github.com/AlfnRU/roundcube-password_recovery/",
"license": "GPL",
"version": "1.0",
"authors": [
{
"name": "Alexander Alferov",
"email": "a@alfn.ru",
"role": "Lead"
}
],
"repositories": [
{
"type": "composer",
"url": "https://plugins.roundcube.net"
}
],
"require": {
"roundcube/plugin-installer": ">=0.1.6"
}
}

65
config.inc.php.dist Normal file
View File

@ -0,0 +1,65 @@
<?php
// Database connection string and table name with user passwords
$config['pr_db_dsn'] = 'mysql://_USER_:_PASSWORD_@localhost/postfix';
$config['pr_users_table'] = 'mailbox';
// Array with names for ext_fields in 'pr_users_table'
// When using the postfix database mailbox table, you must add two fields to this table: 'question' and 'answer'
//
// Execute this query in the postfix database:
//
// ALTER TABLE mailbox ADD question VARCHAR(255) CHARACTER SET utf8 NOT NULL AFTER email_other;
// ALTER TABLE mailbox ADD answer VARCHAR(255) CHARACTER SET utf8 NOT NULL AFTER question;
//
$config['pr_fields'] = [
'altemail' => 'email_other',
'phone' => 'phone',
'question' => 'question',
'answer' => 'answer',
];
// Admin email (this account will receive alerts when an user does not have an alternative email and phone)
$config['pr_admin_email'] = 'postmaster@your.domain.com';
// Use secret question/answer to confirmation password recovery
$config['pr_use_question'] = false;
// Use message with code to confirmation password recovery
$config['pr_use_confirm_code'] = true;
// Minimum length of new password
$config['pr_password_minimum_length'] = 8;
// Confirmation code length
$config['pr_confirm_code_length'] = 6;
// Maximum number of attempts to send confirmation code
$config['pr_confirm_code_count_max'] = 3;
// Confirmation code duration (in minutes)
$config['pr_confirm_code_validity_time'] = 30;
// SMTP settings
// $config['pr_default_smtp_server'] = 'tls://your.domain.com';
// $config['pr_default_smtp_user'] = 'no-reply@your.domain.com';
// $config['pr_default_smtp_pass'] = 'YOUR_SMTP_USER_PASSWORD';
$config['pr_default_smtp_server'] = 'localhost';
$config['pr_default_smtp_user'] = '';
$config['pr_default_smtp_pass'] = '';
// Full path to SMS send function
// This function must accept 2 parameters: phone number and message,
// and return true on success or false on failure
//
// Example of send SMS function using Clickatell HTTP API - see /lib/send.php
//
$config['pr_sms_send_function'] = dirname(__FILE__) . '/bin/sendsms.sh';
// Enables logging of password changes into /logs/password.log
$config['pr_password_log'] = true;
// Set to TRUE if you need write debug messages into /log/console.log
$config['pr_debug'] = false;
?>

132
lib/password.php Normal file
View File

@ -0,0 +1,132 @@
<?php
/*******************************************
*
* Function for save with Password plugin
*
*******************************************/
define('PASSWORD_SUCCESS', 0);
define('PASSWORD_CRYPT_ERROR', 1);
define('PASSWORD_ERROR', 2);
define('PASSWORD_CONNECT_ERROR', 3);
define('PASSWORD_IN_HISTORY', 4);
define('PASSWORD_CONSTRAINT_VIOLATION', 5);
define('PASSWORD_COMPARE_CURRENT', 6);
define('PASSWORD_COMPARE_NEW', 7);
class password_recovery_pwd {
private $rc;
private $pr;
private $drivers = [];
function __construct($pr_plugin) {
$this->pr = $pr_plugin;
$this->rc = $pr_plugin->rc;
}
function _check_strength($passwd)
{
$min_score = ($this->pr->use_password ? $this->rc->config->get('password_minimum_score') : $this->rc->config->get('pr_password_minimum_score'));
if (!$min_score) {
return;
}
if ($this->pr->use_password && ($driver = $this->_load_driver('strength')) && method_exists($driver, 'check_strength')) {
list($score, $reason) = $driver->check_strength($passwd);
} else {
$score = (!preg_match("/[0-9]/", $passwd) || !preg_match("/[^A-Za-z0-9]/", $passwd)) ? 1 : 5;
}
if ($score < $min_score) {
return $this->pr->gettext('password_check_failed') . (!empty($reason) ? " $reason" : '');
}
}
function _save($passwd, $username)
{
if ($res = $this->_check_strength($passwd)) {
return $res;
}
if (!($driver = $this->_load_driver())) {
return $this->pr->gettext('write_failed');
}
$result = $driver->save('', $passwd, $username);
$message = '';
if (is_array($result)) {
$message = $result['message'];
$result = $result['code'];
}
switch ($result) {
case PASSWORD_SUCCESS:
return PASSWORD_SUCCESS;
case PASSWORD_CRYPT_ERROR:
$reason = $this->pr->gettext('crypt_error');
break;
case PASSWORD_CONNECT_ERROR:
$reason = $this->pr->gettext('connect_error');
break;
case PASSWORD_IN_HISTORY:
$reason = $this->pr->gettext('password_in_history');
break;
case PASSWORD_CONSTRAINT_VIOLATION:
$reason = $this->pr->gettext('password_const_viol');
break;
case PASSWORD_ERROR:
default:
$reason = $this->pr->gettext('write_failed');
}
if ($message) {
$reason .= ' ' . $message;
}
return $reason;
}
function _load_driver($type = 'password')
{
if (!($type && $driver = $this->rc->config->get('password_' . $type . '_driver'))) {
$driver = $this->rc->config->get('password_driver', 'sql');
}
if (empty($this->drivers[$type])) {
$class = "rcube_{$driver}_password";
$file = __DIR__ . "/../../password/drivers/$driver.php";
if (!file_exists($file)) {
rcube::raise_error([
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Driver file does not exist ($file)"
], true, false
);
return false;
}
include_once $file;
if (!class_exists($class, false) || (!method_exists($class, 'save') && !method_exists($class, 'check_strength'))) {
rcube::raise_error([
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Broken driver $driver"
], true, false
);
return false;
}
$this->drivers[$type] = new $class;
}
return $this->drivers[$type];
}
}
?>

195
lib/send.php Normal file
View File

@ -0,0 +1,195 @@
<?php
class password_recovery_send {
private $rc;
private $pr;
private $user;
function __construct($pr_plugin) {
$this->pr = $pr_plugin;
$this->rc = $pr_plugin->rc;
$this->user = $pr_plugin->user;
}
// Send SMS over Clickatell API
function send_sms_clickatell($to, $message) {
$clickatell_api_id = 'CHANGEME';
$clickatell_user = 'CHANGEME';
$clickatell_password = 'CHANGEME';
$clickatell_sender = 'CHANGEME';
$url = 'https://api.clickatell.com/http/sendmsg?api_id=%s&user=%s&password=%s&to=%s&from=%s&text=%s';
$url = sprintf($url, $clickatell_api_id, $clickatell_user, $clickatell_password, $to, $clickatell_sender, urlencode($message));
$result = file_get_contents($url);
return $result !== false;
}
// Send SMS
function send_sms($to, $message) {
$ret = false;
$to = escapeshellarg($to);
$message = escapeshellarg($message);
$sms_send_function = $this->rc->config->get('pr_sms_send_function');
if ($sms_send_function) {
if (is_file($sms_send_function)) {
$ret = (int) exec("$sms_send_function $to $message");
} else if (is_callable($sms_send_function)) {
$ret = $sms_send_function($to, $message);
}
}
return $ret !== false || $ret > 0;
}
// Send E-Mail
function send_email($to, $from, $subject, $body) {
$ctb = md5(rand() . microtime());
$subject = "=?UTF-8?B?".base64_encode($subject)."?=";
$headers = "Return-Path: $from\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: multipart/alternative; boundary=\"=_$ctb\"\r\n";
$headers .= "Date: " . date('r', time()) . "\r\n";
$headers .= "From: $from\r\n";
$headers .= "To: $to\r\n";
$headers .= "Subject: $subject\r\n";
$headers .= "Reply-To: $from\r\n";
$txt_body = "--=_$ctb";
$txt_body .= "\r\n";
$txt_body .= "Content-Transfer-Encoding: 7bit\r\n";
$txt_body .= "Content-Type: text/plain; charset=" . $this->rc->config->get('default_charset', RCUBE_CHARSET) . "\r\n";
$h2t = new rcube_html2text($body, false, true, 0);
$txt = rcube_mime::wordwrap($h2t->get_text(), $this->rc->config->get('line_length', 75), "\r\n");
$txt = wordwrap($txt, 998, "\r\n", true);
$txt_body .= "$txt\r\n";
$txt_body .= "--=_$ctb";
$txt_body .= "\r\n";
$msg_body = "Content-Type: multipart/alternative; boundary=\"=_$ctb\"\r\n\r\n";
$msg_body .= $txt_body;
$msg_body .= "Content-Transfer-Encoding: quoted-printable\r\n";
$msg_body .= "Content-Type: text/html; charset=" . $this->rc->config->get('default_charset', RCUBE_CHARSET) . "\r\n\r\n";
$msg_body .= str_replace("=","=3D",$body);
$msg_body .= "\r\n\r\n";
$msg_body .= "--=_$ctb--";
$msg_body .= "\r\n\r\n";
// send message
if (!is_object($this->rc->smtp)) {
$this->rc->smtp_init(true);
}
if($this->rc->config->get('smtp_pass') == "%p") {
$this->rc->config->set('smtp_server', $this->rc->config->get('pr_default_smtp_server'));
$this->rc->config->set('smtp_user', $this->rc->config->get('pr_default_smtp_user'));
$this->rc->config->set('smtp_pass', $this->rc->config->get('pr_default_smtp_pass'));
}
$this->rc->smtp->connect();
if($this->rc->smtp->send_mail($from, $to, $headers, $msg_body)) {
return true;
} else {
rcube::write_log('errors', 'response:' . print_r($this->rc->smtp->get_response(),true));
rcube::write_log('errors', 'errors:' . print_r($this->rc->smtp->get_error(),true));
return false;
}
}
// Send message to administrator
function send_alert_to_admin($user_requesting_new_password) {
$file = dirname(__FILE__) . "/../localization/" . $this->rc->user->language . "/alert_for_admin_to_reset_pw.html";
$body = strtr(file_get_contents($file), array('[USER]' => $user_requesting_new_password));
$subject = $this->pr->gettext('email_subject_admin');
return $this->send_email(
$this->rc->config->get('pr_admin_email'),
$this->get_email_from($user_requesting_new_password),
$subject,
$body
);
}
// Send code to user
function send_confirm_code_to_user() {
$send_email = false;
$send_sms = false;
$confirm_code = $this->generate_confirm_code();
if ($confirm_code && $this->pr->set_user_props(['token'=>$confirm_code])) {
// send EMail
if ($this->user['have_altemail']) {
$file = dirname(__FILE__) . "/../localization/" . $this->rc->user->language . "/reset_pw_body.html";
$link = "http://{$_SERVER['SERVER_NAME']}/?_task=login&_action=plugin.password_recovery&_username=". $this->user['username'];
$body = strtr(file_get_contents($file), ['[LINK]' => $link, '[CODE]' => $confirm_code]);
$subject = $this->pr->gettext('email_subject');
$send_email = $this->send_email(
$this->user['altemail'],
$this->get_email_from($this->rc->config->get('pr_admin_email')),
$subject,
$body
);
}
// send SMS
if ($this->user['have_phone']) {
$send_sms = $this->send_sms(
$this->user['phone'],
$this->pr->gettext('code') . ": " . $confirm_code
);
}
// log & message
if ($send_email || $send_sms) {
$log = "Send password recovery code [". $confirm_code . "] for '" . $this->user['username'] . "'";
$message = $this->pr->gettext('check_account');
if ($send_email) {
$log .= " to alt email: '" . $this->user['altemail'] . "'";
$message .= $this->pr->gettext('check_email');
}
if ($send_sms) {
if ($send_email) {
$log .= " and";
$message .= $this->pr->gettext('and');
}
$log .= " to phone: '" . $this->user['phone'] . "'";
$message .= $this->pr->gettext('check_sms');
}
$this->pr->logging($log);
} else {
$this->pr->set_user_props(['token'=>'', 'token_validity'=>'']);
}
} else {
$message = $this->pr->gettext('write_failed');
}
return [
'send' => ($send_email || $send_sms),
'message' => $message
];
}
// Generate and return a random code
function generate_confirm_code() {
$code_length = (int) $this->rc->config->get('pr_confirm_code_length', 6);
$code = "";
$possible = "0123456789";
while (strlen($code) < $code_length) {
$random = random_int(0, strlen($possible)-1);
$char = substr($possible, $random, 1);
$code .= $char;
$possible = str_replace($char,"",$possible); //removing the used character from the possible
}
return $code;
}
function get_email_from($email) {
$parts = explode('@',$email);
return 'no-reply@'.$parts[1];
}
}
?>

58
localization/en_US.inc Normal file
View File

@ -0,0 +1,58 @@
<?php
$labels['forgot_password'] = 'Forgot password?';
$labels['recovery_password'] = 'Recovery password';
$labels['phone'] = 'Phone';
$labels['altemail'] = 'Alternate E-mail';
$labels['newpassword'] = 'New password';
$labels['newpassword_confirm'] = 'Confirmation';
$labels['code'] = 'Confirmation code for recovery password';
$labels['question'] = 'Secret question';
$labels['answer'] = 'Answer to a secret question';
$labels['answer_confirm'] = 'Confirmation';
$labels['recovery'] = 'Recovery';
$labels['save'] = 'Save';
$labels['cancel'] = 'Cancel';
$labels['email_subject'] = 'Restore mail access';
$labels['email_subject_admin'] = 'Password recovery request';
$labels['click_here'] = 'Click here';
$labels['no_identities'] = 'Complete your password recovery information.';
$labels['no_username'] = 'Enter your user name (e-mail address)';
$labels['renew_code'] = 'Send a new confirmation code';
$labels['count_send_code_maximum'] = 'Maximum number of confirmation code submissions exceeded';
$labels['no_code'] = 'Enter confirmation code';
$labels['no_answer'] = 'Enter a secret answer';
$labels['no_password'] = 'Enter new password';
$labels['no_password_confirm'] = 'Enter password confirmation';
$labels['password_inconsistency'] = 'Password and confirmation do not match';
$labels['password_too_short'] = 'The password must be at least %d characters long.';
$messages['disabled'] = 'The system is currently being maintained and password recovery is not possible. Everything should return to normal soon. We apologize for the inconvenience.';
$messages['banned'] = 'Access is temporarily blocked (too many failed login attempts). Please try again later or contact your administrator.';
$messages['no_identities'] = 'The settings required for password recovery are not configured. %s and configure!';
$messages['user_not_found'] = 'User not found';
$messages['check_account_notice'] = 'The password recovery code has already been sent to you. Check your email and phone';
$messages['check_account'] = 'Password recovery code has been sent to you ';
$messages['check_email'] = 'at an additional email address ';
$messages['check_sms'] = 'on the phone';
$messages['and'] = 'and ';
$messages['sent_to_admin'] = 'No password recovery information was found, so your request was sent to the administrator.';
$messages['send_failed'] = 'Error sending code to restore password. Try again later.';
$messages['write_failed'] = 'Error writing data. Contact your administrator.';
$messages['code_expired'] = 'Confirmation code has expired';
$messages['code_failed'] = 'Invalid confirmation code';
$messages['answer_failed'] = 'Incorrect answer to secret question';
$messages['password_invalid'] = 'Invalid password';
$messages['password_check_failed'] = 'Error. The password is too simple!';
$messages['password_forbidden'] = 'The password contains invalid characters';
$messages['password_changed'] = 'Password changed successfully!';
$messages['password_not_changed'] = 'An error occurred while changing your password. Please try again later or contact your administrator.';
$messages['crypt_error'] = 'I cannot save my new password. Missing cryptographic function.';
$messages['connect_error'] = 'I cannot save my new password. Connection error.';
$messages['password_in_history'] = 'This password has already been used.';
$messages['password_const_viol'] = 'Password restriction violation. The password may be too weak.';
$messages['phone_invalid'] = 'Wrong phone number! Number must be %d digits';
$messages['altemail_invalid'] = 'The alternate email address is incorrect!';
$messages['altemail_match_primary'] = 'The secondary email address must not be the same as the primary email address!';
?>

View File

@ -0,0 +1,2 @@
<p>User [USER] requested a password change but they do not have an phone and alternate email address registered.</p>
<p>Please change the password or register an phone and/or alternative email for this user.</p>

View File

@ -0,0 +1,3 @@
<p>Hello!</p>
<p>You have requested password recovery.</p>
<p>Confirmation code: [CODE]</p>

58
localization/ru_RU.inc Normal file
View File

@ -0,0 +1,58 @@
<?php
$labels['forgot_password'] = 'Забыли пароль?';
$labels['recovery_password'] = 'Восстановление пароля';
$labels['phone'] = 'Телефон';
$labels['altemail'] = 'Альтернативный E-mail';
$labels['newpassword'] = 'Новый пароль';
$labels['newpassword_confirm'] = 'Подтверждение';
$labels['code'] = 'Код для восстановления пароля';
$labels['question'] = 'Секретный вопрос';
$labels['answer'] = 'Ответ на секретный вопрос';
$labels['answer_confirm'] = 'Подтверждение';
$labels['recovery'] = 'Восстановить';
$labels['save'] = 'Записать';
$labels['cancel'] = 'Отмена';
$labels['email_subject'] = 'Восстановление доступа к почте';
$labels['email_subject_admin'] = 'Запрос на восстановление пароля';
$labels['click_here'] = 'Щелкните здесь';
$labels['no_identities'] = 'Заполните реквизиты для восстановления пароля.';
$labels['no_username'] = 'Введите имя пользователя (адрес электронной почты)';
$labels['renew_code'] = 'Отправить новый код подтверждения';
$labels['count_send_code_maximum'] = 'Превышено максимальное количество отправок кода подтверждения';
$labels['no_code'] = 'Введите код подтверждения';
$labels['no_answer'] = 'Введите ответ на секретный вопрос';
$labels['no_password'] = 'Введите новый пароль';
$labels['no_password_confirm'] = 'Введите подтверждение пароля';
$labels['password_inconsistency'] = 'Пароль и подтверждение не совпадают';
$labels['password_too_short'] = 'Длина пароля должна быть как минимум %d символов.';
$messages['disabled'] = 'В настоящее время проводится обслуживание системы и восстановление пароля невозможно. Все должно вернуться в нормальное состояние в ближайшее время. Приносим свои извинения за причиненные неудобства.';
$messages['banned'] = 'Доступ временно заблокирован (слишком много неудачных попыток войти). Пожалуйста, попробуйте еще раз позже или обратитесь к администратору.';
$messages['no_identities'] = 'Не настроены параметры, требуемые для восстановления пароля. %s и настройте!';
$messages['user_not_found'] = 'Пользователь не найден';
$messages['check_account_notice'] = 'Код для восстановления пароля уже был отправлен Вам ранее. Проверьте свою почту и телефон';
$messages['check_account'] = 'Код для восстановления пароля был отправлен Вам ';
$messages['check_email'] = 'на дополнительный адрес электронной почты ';
$messages['check_sms'] = 'на телефон';
$messages['and'] = 'и ';
$messages['sent_to_admin'] = 'Не найдены реквизиты для восстановления пароля, поэтому Ваш запрос был передан администратору.';
$messages['send_failed'] = 'Ошибка при отправке кода для восстановлению пароля. Попробуйте еще раз позже.';
$messages['write_failed'] = 'Ошибка при записи данных. Обратитесь к администратору.';
$messages['code_expired'] = 'Срок действия кода подтверждения истек';
$messages['code_failed'] = 'Неверный код подтверждения';
$messages['answer_failed'] = 'Неверный ответ на секретный вопрос';
$messages['password_invalid'] = 'Неверный пароль';
$messages['password_check_failed'] = 'Ошибка. Слишком простой пароль!';
$messages['password_forbidden'] = 'Пароль содержит недопустимые символы.';
$messages['password_changed'] = 'Пароль успешно изменен!';
$messages['password_not_changed'] = 'Произошла ошибка при изменении вашего пароля. Попробуйте еще раз позже или обратитесь к администратору.';
$messages['crypt_error'] = 'Не могу сохранить новый пароль. Отсутствует криптографическая функция.';
$messages['connect_error'] = 'Не могу сохранить новый пароль. Ошибка соединения.';
$messages['password_in_history'] = 'Этот пароль уже использовался ранее.';
$messages['password_const_viol'] = 'Нарушение ограничения пароля. Вероятно, слишком слабый пароль.';
$messages['phone_invalid'] = 'Неверный номер телефона! Номер должен состоять из %d цифр';
$messages['altemail_invalid'] = 'Неверный адрес альтернативной электронной почты!';
$messages['altemail_match_primary'] = 'Дополнительный адрес электронной почты не должен совпадать с основным!';
?>

View File

@ -0,0 +1,2 @@
<p>Пользователь <a href=mailto:[USER]>[USER]</a> запросил(а) сброс пароля, но у него/неё не найдены параметры для восстановления пароля.</p>
<p>Пожалуйста, поменяйте пароль пользователю или настройте дополнительный адрес электронной почты и/или телефон для него/неё.</p>

View File

@ -0,0 +1,3 @@
<p>Здравствуйте!</p>
<p>Вы запросили восстановление пароля.</p>
<p>Код подтверждения: [CODE]</p>

94
password_recovery.js Normal file
View File

@ -0,0 +1,94 @@
if (window.rcmail) {
rcmail.addEventListener('init', function(evt) {
var loginform = $('#login-form');
if (loginform) {
loginform.append('<a class="home" id="password_forgot" href="javascript:forgot_password();">' + rcmail.gettext('forgot_password','password_recovery') + '</a>');
}
var newpasswordform = $('#new-password-form');
if (newpasswordform && rcmail.env.pr_use_confirm_code) {
newpasswordform.append('<a class="home" id="renew_confirm_code" href="javascript:renew_confirm_code();">' + rcmail.gettext('renew_code','password_recovery') + '</a>');
}
rcmail.register_command('plugin.password_recovery.cancel', function() {
rcmail.http_request('plugin.password_recovery', { '_a':'cancel' });
}, true);
rcmail.register_command('plugin.password_recovery.reset', function() {
var input_username = rcube_find_object('_username');
if (input_username && input_username.value == '') {
rcmail.alert_dialog(rcmail.get_label('no_username', 'password_recovery'), function() {
input_username.focus();
return true;
});
}
else {
rcmail.gui_objects.recoverypasswordform.submit();
}
}, true);
rcmail.register_command('plugin.password_recovery.save', function() {
var input_code = rcube_find_object('_code'),
input_answer = rcube_find_object('_answer'),
input_newpassword = rcube_find_object('_newpassword'),
input_newpassword_confirm = rcube_find_object('_newpassword_confirm');
if (rcmail.env.pr_use_confirm_code && input_code && input_code.value == '') {
rcmail.alert_dialog(rcmail.get_label('no_code', 'password_recovery'), function() {
input_code.focus();
return true;
});
}
else if (rcmail.env.pr_use_question && input_answer && input_answer.value == '') {
rcmail.alert_dialog(rcmail.get_label('no_answer', 'password_recovery'), function() {
input_answer.focus();
return true;
});
}
else if (input_newpassword && input_newpassword.value == '') {
rcmail.alert_dialog(rcmail.get_label('no_password', 'password_recovery'), function() {
input_newpassword.focus();
return true;
});
}
else if (input_newpassword_confirm && input_newpassword_confirm.value == '') {
rcmail.alert_dialog(rcmail.get_label('no_password_confirm', 'password_recovery'), function() {
input_newpassword_confirm.focus();
return true;
});
}
else if (input_newpassword && input_newpassword_confirm && input_newpassword.value != input_newpassword_confirm.value) {
rcmail.alert_dialog(rcmail.get_label('password_inconsistency', 'password_recovery'), function() {
input_newpassword.focus();
return true;
});
}
else if (input_newpassword && input_newpassword.value.length < rcmail.env.pr_password_minimum_length) {
rcmail.alert_dialog(rcmail.get_label('password_too_short', 'password_recovery').replace('%d', minimum_length), function() {
input_newpassword.focus();
return true;
});
}
else {
rcmail.gui_objects.newpasswordform.submit();
}
}, true);
$('input:not(:hidden)').first().focus();
});
}
function forgot_password() {
var url = "./?_task=login&_action=plugin.password_recovery";
/* var input_user = rcube_find_object('_user');
if (input_user && input_user.value != '') {
url = url + "&_u=" + input_user.value;
}*/
document.location.href = url;
}
function renew_confirm_code() {
rcmail.http_request('plugin.password_recovery', { '_a':'renew', '_username':rcmail.env.pr_username });
}

510
password_recovery.php Normal file
View File

@ -0,0 +1,510 @@
<?php
/**
* Password recovery
*
* Plugin to reset an account password
*
* @version 1.0
* @original_author Alexander Alferov
*
* @url https://github.com/AlfnRU/roundcube-password_recovery
*/
class password_recovery extends rcube_plugin {
public $task = 'login|logout|settings|mail';
public $rc;
public $db;
public $user;
public $use_password;
public $pwd_conf;
private $pwd;
private $fields;
private $send;
function init() {
$this->rc = rcmail::get_instance();
$this->load_config();
if (!$this->rc->config->get('pr_use_confirm_code') && !$this->rc->config->get('pr_use_question'))
return;
$this->init_ui();
$this->add_texts('localization/');
if ($this->rc->task == 'login' || $this->rc->task == 'logout') {
$this->add_hook('render_page', [$this, 'add_labels_to_login_page']);
$this->add_hook('startup', [$this, 'startup']);
$this->register_action('plugin.get_confirm_code_count', [$this, 'get_confirm_code_count']);
} else if ($this->rc->task == 'mail') {
$this->add_hook('render_page', [$this, 'add_labels_to_mail_page']);
$this->add_hook('messages_list', [$this, 'check_identities']);
} else if ($this->rc->task == 'settings') {
$this->add_hook('identity_form', [$this, 'identity_form']);
$this->add_hook('identity_update', [$this, 'identity_update']);
}
$this->include_script('password_recovery.js');
}
/*******************
* STARTUP
*******************/
function init_ui() {
if (!$this->fields) $this->fields = $this->rc->config->get('pr_fields');
if (!$this->db) $this->get_dbh();
if (!$this->user) $this->get_user_props();
$this->use_password = ($this->rc->config->get('pr_use_password_plugin') && $this->rc->plugins->load_plugin('password', true));
foreach($this->fields as $field => $field_name){
$query = "SELECT " . $field_name . " FROM " . $this->rc->config->get('pr_users_table');
$result = $this->db->query($query);
if (!$result) {
$type = ($field == 'phone' ? 'VARCHAR(30)' : 'VARCHAR(255)');
$query = "ALTER TABLE " . $this->rc->config->get('pr_users_table') . " ADD " . $field_name . " " . $type . " CHARACTER SET utf8 NOT NULL";
$result = $this->db->query($query);
}
}
require_once $this->home . '/lib/send.php';
$this->send = new password_recovery_send($this);
require_once $this->home . '/lib/password.php';
$this->pwd = new password_recovery_pwd($this);
}
function startup($p) {
if ($this->rc->action != 'plugin.password_recovery' || !isset($_SESSION['temp']))
return $p;
switch ($this->get_action()) {
case 'init':
$this->recovery_password_form();
break;
case 'renew':
$this->renew_confirm_code();
break;
case 'new':
$this->new_password_form();
break;
case 'reset':
$this->reset_password();
break;
case 'save':
$this->save_password();
break;
case 'cancel':
$this->rc->kill_session();
$this->rc->output->command('redirect', './');
break;
}
return $p;
}
function add_labels_to_login_page($p) {
if ($p['template'] == 'login') {
$this->rc->output->add_label('password_recovery.forgot_password');
}
return $p;
}
function add_labels_to_mail_page($p) {
$this->rc->output->add_label('password_recovery.no_identities');
$this->rc->output->add_script('rcmail.message_time = 10000;');
return $p;
}
/*******************
* PASSWORD
*******************/
// Creating form for reset password
private function recovery_password_form() {
$this->rc->output->add_label(
'password_recovery.recovery_password',
'password_recovery.no_username'
);
$this->rc->output->set_pagetitle($this->gettext('recovery_password'));
$this->rc->output->add_gui_object('recoverypasswordform', 'recovery-password-form');
$this->rc->output->send('password_recovery.recovery_password_form');
}
// Creating form for new password
private function new_password_form() {
$this->rc->output->add_label(
'password_recovery.recovery_password',
'password_recovery.renew_code',
'password_recovery.count_send_code_maximum',
'password_recovery.no_code',
'password_recovery.no_answer',
'password_recovery.no_password',
'password_recovery.no_password_confirm',
'password_recovery.password_inconsistency',
'password_recovery.password_too_short'
);
$this->rc->output->set_pagetitle($this->gettext('recovery_password'));
$this->rc->output->add_gui_object('newpasswordform', 'new-password-form');
$password_minimum_length = ($this->use_password ? $this->rc->config->get('password_minimum_length',8) : $this->rc->config->get('pr_password_minimum_length',8));
$this->rc->output->set_env('pr_username', $this->user['username']);
$this->rc->output->set_env('pr_question', $this->user['question']);
$this->rc->output->set_env('pr_use_question', (bool) ($this->rc->config->get('pr_use_question') && $this->user['have_answer']));
$this->rc->output->set_env('pr_use_confirm_code', (bool) ($this->rc->config->get('pr_use_confirm_code') && $this->user['have_code']));
$this->rc->output->set_env('pr_password_minimum_length', (int) $password_minimum_length);
$this->rc->output->send('password_recovery.new_password_form');
}
// Renew and send confirmation code to user (to alternative email and phone)
private function renew_confirm_code() {
if ($this->get_confirm_code_count() < $this->rc->config->get('pr_confirm_code_count_max')) {
$result = $this->send->send_confirm_code_to_user();
if ($result['send']) {
$this->update_confirm_code_count(1);
$message = $result['message'];
$type = 'confirmation';
} else {
$message = $this->gettext('send_failed') . "\n" . $result['message'];
$type = 'error';
}
} else {
$message = $this->gettext('count_send_code_maximum');
$type = 'error';
}
$this->rc->output->command('display_message', $message, $type);
$this->rc->output->send('plugin');
}
// Creating and send confirmation code to user (to alternative email and phone) or send message to administrator
private function reset_password() {
// kill remember_me cookies
setcookie ('rememberme_user', '', time()-3600);
setcookie ('rememberme_pass', '', time()-3600);
$allow_answer = ($this->rc->config->get('pr_use_question') && $this->user['have_answer']);
$allow_code = ($this->rc->config->get('pr_use_confirm_code') && $this->user['have_code']);
if (!$this->user['username']) {
$message = $this->gettext('user_not_found');
$type = 'error';
} else if (!$allow_answer && !$allow_code) {
$this->send->send_alert_to_admin($this->user['username']);
$message = $this->gettext('sent_to_admin');
$type = 'error';
} else if ($allow_code) {
$result['send'] = false;
if ($this->user['token_validity'] && !$this->user['token_expired']) {
$this->update_confirm_code_count(1);
$result['send'] = true;
$message = $this->gettext('check_account_notice');
$type = 'notice';
} else {
$result = $this->send->send_confirm_code_to_user();
if ($result['send']) {
$this->update_confirm_code_count(1);
$message = $result['message'];
$type = 'confirmation';
} else {
$message = $this->gettext('send_failed') . "\n" . $result['message'];
$type = 'error';
}
}
}
$this->logging("Password recovery request for '" . $this->user['username'] . "' (IP: " . rcube_utils::remote_addr() . ")");
if ($message) {
$this->rc->output->command('display_message', $message, $type);
}
if ($type == 'error') {
$this->recovery_password_form();
} else {
$this->new_password_form();
}
}
// Save new password to DB
private function save_password() {
$params = rcube_utils::request2param(rcube_utils::INPUT_POST);
$this->debug("Save new password: " . print_r($params, true));
if ($this->rc->config->get('pr_use_question') && $this->user['have_answer'] && $this->user['answer'] != $params['answer']) {
$message = $this->gettext('answer_failed');
$type = 'error';
} else if ($this->rc->config->get('pr_use_confirm_code') && $this->user['token_expired']) {
$message = $this->gettext('code_expired');
$type = 'error';
} else if ($this->rc->config->get('pr_use_confirm_code') && $this->user['token'] != $params['code']) {
$message = $this->gettext('code_failed');
$type = 'error';
} else {
// props to save
$save = ['token'=>'', 'token_validity'=>''];
// check allowed characters according to the configured 'password_charset' option
// by converting the password entered by the user to this charset and back to UTF-8
$rc_charset = strtoupper($this->rc->output->get_charset());
$orig_pwd = $params['newpassword'];
$chk_pwd = rcube_charset::convert($orig_pwd, $rc_charset, 'UTF-8');
$chk_pwd = rcube_charset::convert($chk_pwd, 'UTF-8', $rc_charset);
// We're doing this for consistence with Roundcube core
$newpassword = rcube_charset::convert($params['newpassword'], $rc_charset, 'UTF-8');
if ($chk_pwd != $orig_pwd || preg_match('/[\x00-\x1F\x7F]/', $newpassword)) {
$message = $this->gettext('password_forbidden');
$type = 'error';
} else if (!$this->use_password && ($chk_strength = $this->pwd->_check_strength($newpassword))) {
$message = $chk_strength;
$type = 'error';
} else {
if ($this->use_password) {
$result = $this->pwd->_save($newpassword, $this->user['username']);
if ($result != 0) {
$message = $this->gettext('write_failed') . ": " . $result;
$type = 'error';
}
} else {
$save['password'] = crypt($newpassword, '$1$' . rcube_utils::random_bytes(9));
}
if ($type != 'error' && $this->set_user_props($save)) {
$this->logging("Save new password for '" . $this->user['username'] . "' (IP: " . rcube_utils::remote_addr() . ")");
$message = $this->gettext('password_changed');
$type = 'confirmation';
} else if (!$this->use_password) {
$message = $this->gettext('password_not_changed');
$type = 'error';
}
}
}
$this->rc->output->command('display_message', $message, $type);
if ($type != 'error') {
$this->rc->kill_session();
$this->rc->output->command('redirect', './', 2);
// $this->rc->output->send('login');
}
}
/*******************
* IDENTITIES
*******************/
// Verifying the user identities needed to recover the password
function check_identities() {
if (!isset($_SESSION['show_notice_identities']) && !$this->user['have_altemail'] && !$this->user['have_phone']) {
$link = "<a href='./?_task=settings&_action=identities'>". $this->gettext('click_here') ."</a>";
$this->rc->output->command('display_message', sprintf($this->gettext('no_identities'), $link), 'notice');
$_SESSION['show_notice_identities'] = true;
}
}
// Handler for 'identity_form' hook (executed on identities form create)
function identity_form($p) {
if (isset($p['form']['addressing']) && !empty($p['record']['identity_id'])) {
$new_fields = [];
foreach ($p['form']['addressing']['content'] as $col => $colprop) {
$new_fields[$col] = $colprop;
if ($col == 'email') {
// add ext fields after 'email'
foreach ($this->fields as $field => $field_name){
$new_fields[$field] = ['type' => 'text', 'size' => 40, 'label' => $this->gettext($field)];
}
}
}
$p['form']['addressing']['content'] = $new_fields;
if($this->user['username']){
foreach ($this->fields as $field => $field_name){
$p['record'][$field] = $this->user[$field];
}
}
}
return $p;
}
// Handler for identity_update hook (executed on identities form submit)
function identity_update($p) {
$save = [];
foreach ($this->fields as $field => $field_name) {
$save[$field] = rcube_utils::get_input_value("_".$field,rcube_utils::INPUT_POST);
}
foreach ($save as $par => $val) {
if ((!$this->user['username'] && empty($val)) || ($this->user['username'] && $val == $this->user[$par])) {
unset($save[$par]);
}
}
if ($save['altemail']) {
$save['altemail'] = rcube_utils::idn_to_ascii($save['altemail']);
if (empty($save['altemail'])) {
$this->rc->output->command('display_message', $this->gettext('altemail_cleared'), 'confirmation');
} else if($save['altemail'] == $p['record']['email']) {
unset($save['altemail']);
$p['abort'] = true;
$p['message'] = $this->gettext('altemail_match_primary');
return $p;
} else if(!rcube_utils::check_email($save['altemail'])) {
unset($save['altemail']);
$p['abort'] = true;
$p['message'] = $this->gettext('altemail_invalid');
return $p;
}
}
if (count($save)) {
$this->debug("Save user identities: " . print_r($save, true));
$this->set_user_props($save);
}
return $p;
}
// Return array - user props (username must be user@domain.ltd)
function get_user_props($username = null, $with_alias = true) {
if (!$username) {
$username = ($this->rc->get_user_name() ? $this->rc->get_user_name() : rcube_utils::get_input_value("_username", rcube_utils::INPUT_GPC));
}
$ret = [];
$user = trim(urldecode($username));
if ($user) {
// get user row
$query = "SELECT u.user_id, u.username, i.email" .
" FROM users u" .
" INNER JOIN identities i ON i.user_id = u.user_id" .
" WHERE username=?";
$result = $this->rc->db->query($query, $user);
if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
$fields = [];
foreach ($this->fields as $field => $field_name) {
$fields[] = $field_name . " as " . $field;
}
$query = "SELECT " . implode(",",$fields) . ", token, token_validity, token='' OR token_validity < NOW() as token_expired" .
" FROM " . $this->rc->config->get('pr_users_table') .
" WHERE username=?";
$result = $this->db->query($query, $arr['email']);
$ret = array_merge($arr, $this->db->fetch_assoc($result));
} else {
// for alias (with users_alias plugin)
if ($with_alias && $this->rc->plugins->load_plugin('users_alias', true)) {
$users_alias = new users_alias($this->api);
$result = $users_alias->alias2user(['user' => $user]);
if ($result['user']) {
$ret = $this->get_user_props($result['user'], false);
}
}
}
$_SESSION['username'] = $ret['username']; //for password plugin
$ret['have_altemail'] = ($ret['altemail'] && !empty($ret['altemail']));
$ret['have_phone'] = ($ret['phone'] && !empty($ret['phone']));
$ret['have_code'] = ($ret['have_altemail'] || $ret['have_phone']);
$ret['have_answer'] = ($ret['question'] && !empty($ret['question']) && $ret['answer'] && !empty($ret['answer']));
}
$this->user = $ret;
return $ret;
}
// Save user props to DB
function set_user_props($props) {
$fields = [];
foreach ($this->fields as $field => $field_name) {
if (isset($props[$field])) {
$fields[] = $field_name . " = '" . $props[$field] . "'";
}
}
if (isset($props['token'])) {
if (empty($props['token'])) {
$code_validity_time = 0;
} else {
$code_validity_time = (int) $this->rc->config->get('pr_confirm_code_validity_time', 30);
}
$fields[] = "token = '" . $props['token'] . "', token_validity = NOW() + INTERVAL " . $code_validity_time . " MINUTE";
}
if ($props['password']) {
$fields[] = "password = '" . $props['password'] . "'";
}
if (count($fields)) {
$query = "UPDATE " . $this->rc->config->get('pr_users_table') . " SET " . implode(",",$fields) . " WHERE username=?";
$this->db->query($query, $this->user['username']);
$this->get_user_props(); //update user props
$this->debug("Update user '" . $this->user['username'] . "' props: " . print_r($fields, true));
return $this->db->affected_rows() == 1;
}
return false;
}
function update_confirm_code_count($plus = 0) {
$count = $this->get_confirm_code_count() + $plus;
$_SESSION['pr_confirm_code_count'] = $count;
}
function get_confirm_code_count() {
if (!isset($_SESSION['pr_confirm_code_count'])) {
$_SESSION['pr_confirm_code_count'] = 0;
}
return $_SESSION['pr_confirm_code_count'];
}
/*******************
* service functions
*******************/
function get_dbh() {
if (!$this->db) {
if ($dsn = $this->rc->config->get('pr_db_dsn')) {
$this->db = rcube_db::factory($dsn);
$this->db->set_debug((bool)$this->rc->config->get('sql_debug'));
}
else {
$this->db = $this->rc->get_dbh();
}
}
return $this->db;
}
function get_action() {
$action = rcube_utils::get_input_value('_a', rcube_utils::INPUT_GPC);
if (!$action || empty($action)) {
$action = 'init';
}
return $action;
}
function logging($text) {
if ($this->rc->config->get('pr_password_log') == true) {
rcube::write_log('password', $text);
}
}
function debug($text) {
if ($this->rc->config->get('pr_debug') == true) {
$msg = (is_array($text) ? print_r($text, true) : $text);
rcube::write_log('console', $msg);
}
}
}
?>

View File

@ -0,0 +1,71 @@
<roundcube:include file="includes/layout.html" />
<div id="layout-content" class="selected no-navbar" role="main"><br /><br />
<roundcube:object name="logo" style="top:0px" src="/images/logo.svg" id="logo" alt="Logo" />
<p style="margin-top: 1.5rem; margin-bottom: 1.5rem; font-size: 20px; font-weight: bold;"><roundcube:label name="password_recovery.recovery_password" /></p>
<form id="new-password-form" name="new-password-form" method="post" class="propform">
<input type="hidden" name="_username" value="<roundcube:var name="env:pr_username" />">
<input type="hidden" name="_a" value="save">
<table style="margin-left: auto; margin-right: auto; text-align:right;">
<tbody>
<tr>
<td class="title">
<label for="_newpassword"><roundcube:label name="password_recovery.newpassword" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="password" size="35" maxlength="250" id="_newpassword" name="_newpassword" autocomplete="off">
</td>
</tr>
<tr>
<td class="title">
<label for="_newpassword_confirm"><roundcube:label name="password_recovery.newpassword_confirm" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="password" size="35" maxlength="250" id="_newpassword_confirm" name="_newpassword_confirm" autocomplete="off">
</td>
</tr>
<roundcube:if condition="env:pr_use_question == true" />
<tr><td colspan="2">&nbsp;&nbsp;</td></tr>
<tr>
<td class="title">
<label for="_question"><roundcube:label name="password_recovery.question" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="text" size="35" maxlength="250" id="_question" name="_question" readonly value="<roundcube:var name="env:pr_question" />">
</td>
</tr>
<tr>
<td class="title">
<label for="_answer"><roundcube:label name="password_recovery.answer" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="text" size="35" maxlength="250" id="_answer" name="_answer" autocomplete="off">
</td>
</tr>
<roundcube:endif />
<roundcube:if condition="env:pr_use_confirm_code == true" />
<tr><td colspan="2">&nbsp;&nbsp;</td></tr>
<tr>
<td class="title">
<label for="_code"><roundcube:label name="password_recovery.code" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="text" size="35" maxlength="250" id="_code" name="_code" autocomplete="off">
</td>
</tr>
<roundcube:endif />
</tbody>
</table>
<p class="formbuttons" style="text-align:center;"><br />
<roundcube:button command="plugin.password_recovery.save" label="password_recovery.save" class="button mainaction" />
<span>&nbsp;&nbsp;</span>
<roundcube:button command="plugin.password_recovery.cancel" label="password_recovery.cancel" class="button" />
</p>
</form>
</div>
<noscript>
<p class="noscriptwarning"><roundcube:label name="noscriptwarning" /></p>
</noscript>
<roundcube:include file="includes/footer.html" />

View File

@ -0,0 +1,32 @@
<roundcube:include file="includes/layout.html" />
<div id="layout-content" class="selected no-navbar" role="main"><br /><br />
<roundcube:object name="logo" style="top:0px" src="/images/logo.svg" id="logo" alt="Logo" />
<p style="margin-top: 1.5rem; margin-bottom: 1.5rem; font-size: 20px; font-weight: bold;"><roundcube:label name="password_recovery.recovery_password" /></p>
<form id="recovery-password-form" name="recovery-password-form" method="post" class="propform">
<input type="hidden" name="_a" value="reset">
<table style="margin-left: auto; margin-right: auto; text-align:right;">
<tbody>
<tr>
<td class="title">
<label for="_username"><roundcube:label name="username" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="text" size="35" maxlength="250" id="_username" name="_username" autocomplete="off" value="<roundcube:var name="request:_username" />">
</td>
</tr>
</tbody>
</table>
<p class="formbuttons" style="text-align:center;"><br />
<roundcube:button command="plugin.password_recovery.reset" label="password_recovery.recovery" class="button mainaction" />
<span>&nbsp;&nbsp;</span>
<roundcube:button command="plugin.password_recovery.cancel" label="password_recovery.cancel" class="button" />
</p>
</form>
</div>
<noscript>
<p class="noscriptwarning"><roundcube:label name="noscriptwarning" /></p>
</noscript>
<roundcube:include file="includes/footer.html" />

View File

@ -0,0 +1,71 @@
<roundcube:include file="includes/layout.html" />
<div id="layout-content" class="selected no-navbar" role="main"><br /><br />
<roundcube:object name="logo" style="top:0px" src="/images/logo.svg" id="logo" alt="Logo" />
<p style="margin-top: 1.5rem; margin-bottom: 1.5rem; font-size: 20px; font-weight: bold;"><roundcube:label name="password_recovery.recovery_password" /></p>
<form id="new-password-form" name="new-password-form" method="post" class="propform">
<input type="hidden" name="_username" value="<roundcube:var name="env:pr_username" />">
<input type="hidden" name="_a" value="save">
<table style="margin-left: auto; margin-right: auto; text-align:right;">
<tbody>
<tr>
<td class="title">
<label for="_newpassword"><roundcube:label name="password_recovery.newpassword" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="password" size="35" maxlength="250" id="_newpassword" name="_newpassword" autocomplete="off">
</td>
</tr>
<tr>
<td class="title">
<label for="_newpassword_confirm"><roundcube:label name="password_recovery.newpassword_confirm" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="password" size="35" maxlength="250" id="_newpassword_confirm" name="_newpassword_confirm" autocomplete="off">
</td>
</tr>
<roundcube:if condition="env:pr_use_question == true" />
<tr><td colspan="2">&nbsp;&nbsp;</td></tr>
<tr>
<td class="title">
<label for="_question"><roundcube:label name="password_recovery.question" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="text" size="35" maxlength="250" id="_question" name="_question" readonly value="<roundcube:var name="env:pr_question" />">
</td>
</tr>
<tr>
<td class="title">
<label for="_answer"><roundcube:label name="password_recovery.answer" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="text" size="35" maxlength="250" id="_answer" name="_answer" autocomplete="off">
</td>
</tr>
<roundcube:endif />
<roundcube:if condition="env:pr_use_confirm_code == true" />
<tr><td colspan="2">&nbsp;&nbsp;</td></tr>
<tr>
<td class="title">
<label for="_code"><roundcube:label name="password_recovery.code" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="text" size="35" maxlength="250" id="_code" name="_code" autocomplete="off">
</td>
</tr>
<roundcube:endif />
</tbody>
</table>
<p class="formbuttons" style="text-align:center;"><br />
<roundcube:button command="plugin.password_recovery.save" label="password_recovery.save" class="button mainaction" />
<span>&nbsp;&nbsp;</span>
<roundcube:button command="plugin.password_recovery.cancel" label="password_recovery.cancel" class="button" />
</p>
</form>
</div>
<noscript>
<p class="noscriptwarning"><roundcube:label name="noscriptwarning" /></p>
</noscript>
<roundcube:include file="includes/footer.html" />

View File

@ -0,0 +1,32 @@
<roundcube:include file="includes/layout.html" />
<div id="layout-content" class="selected no-navbar" role="main"><br /><br />
<roundcube:object name="logo" style="top:0px" src="/images/logo.svg" id="logo" alt="Logo" />
<p style="margin-top: 1.5rem; margin-bottom: 1.5rem; font-size: 20px; font-weight: bold;"><roundcube:label name="password_recovery.recovery_password" /></p>
<form id="recovery-password-form" name="recovery-password-form" method="post" class="propform">
<input type="hidden" name="_a" value="reset">
<table style="margin-left: auto; margin-right: auto; text-align:right;">
<tbody>
<tr>
<td class="title">
<label for="_username"><roundcube:label name="username" /></label>&nbsp;&nbsp;
</td>
<td class="input">
<input type="text" size="35" maxlength="250" id="_username" name="_username" autocomplete="off" value="<roundcube:var name="request:_u" />">
</td>
</tr>
</tbody>
</table>
<p class="formbuttons" style="text-align:center;"><br />
<roundcube:button command="plugin.password_recovery.reset" label="password_recovery.recovery" class="button mainaction" />
<span>&nbsp;&nbsp;</span>
<roundcube:button command="plugin.password_recovery.cancel" label="password_recovery.cancel" class="button" />
</p>
</form>
</div>
<noscript>
<p class="noscriptwarning"><roundcube:label name="noscriptwarning" /></p>
</noscript>
<roundcube:include file="includes/footer.html" />