+
From 299faf5402b5e0998e0e6893568db6aee1d82250 Mon Sep 17 00:00:00 2001
From: alf
Date: Sun, 9 Jan 2022 15:50:54 +0300
Subject: [PATCH] ver 1.0
---
.gitignore | 1 +
README.md | 16 +-
bin/sendsms.sh | 54 ++
composer.json | 24 +
config.inc.php.dist | 65 +++
lib/password.php | 132 +++++
lib/send.php | 195 +++++++
localization/en_US.inc | 58 ++
.../en_US/alert_for_admin_to_reset_pw.html | 2 +
localization/en_US/reset_pw_body.html | 3 +
localization/ru_RU.inc | 58 ++
.../ru_RU/alert_for_admin_to_reset_pw.html | 2 +
localization/ru_RU/reset_pw_body.html | 3 +
password_recovery.js | 94 ++++
password_recovery.php | 510 ++++++++++++++++++
.../default/templates/new_password_form.html | 71 +++
.../templates/recovery_password_form.html | 32 ++
.../elastic/templates/new_password_form.html | 71 +++
.../templates/recovery_password_form.html | 32 ++
19 files changed, 1422 insertions(+), 1 deletion(-)
create mode 100644 .gitignore
create mode 100755 bin/sendsms.sh
create mode 100644 composer.json
create mode 100644 config.inc.php.dist
create mode 100644 lib/password.php
create mode 100644 lib/send.php
create mode 100644 localization/en_US.inc
create mode 100644 localization/en_US/alert_for_admin_to_reset_pw.html
create mode 100644 localization/en_US/reset_pw_body.html
create mode 100644 localization/ru_RU.inc
create mode 100644 localization/ru_RU/alert_for_admin_to_reset_pw.html
create mode 100644 localization/ru_RU/reset_pw_body.html
create mode 100644 password_recovery.js
create mode 100644 password_recovery.php
create mode 100644 skins/default/templates/new_password_form.html
create mode 100644 skins/default/templates/recovery_password_form.html
create mode 100644 skins/elastic/templates/new_password_form.html
create mode 100644 skins/elastic/templates/recovery_password_form.html
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e9e3af
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+config.inc.php
diff --git a/README.md b/README.md
index db6a079..cc1d340 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,16 @@
#### 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.
diff --git a/bin/sendsms.sh b/bin/sendsms.sh
new file mode 100755
index 0000000..605e5af
--- /dev/null
+++ b/bin/sendsms.sh
@@ -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
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..9fa232f
--- /dev/null
+++ b/composer.json
@@ -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"
+ }
+}
diff --git a/config.inc.php.dist b/config.inc.php.dist
new file mode 100644
index 0000000..c981f7b
--- /dev/null
+++ b/config.inc.php.dist
@@ -0,0 +1,65 @@
+ '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;
+
+?>
diff --git a/lib/password.php b/lib/password.php
new file mode 100644
index 0000000..9b07f13
--- /dev/null
+++ b/lib/password.php
@@ -0,0 +1,132 @@
+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];
+ }
+
+}
+
+?>
diff --git a/lib/send.php b/lib/send.php
new file mode 100644
index 0000000..86dc6ea
--- /dev/null
+++ b/lib/send.php
@@ -0,0 +1,195 @@
+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];
+ }
+}
+
+?>
diff --git a/localization/en_US.inc b/localization/en_US.inc
new file mode 100644
index 0000000..2f77655
--- /dev/null
+++ b/localization/en_US.inc
@@ -0,0 +1,58 @@
+
diff --git a/localization/en_US/alert_for_admin_to_reset_pw.html b/localization/en_US/alert_for_admin_to_reset_pw.html
new file mode 100644
index 0000000..4f8a2b6
--- /dev/null
+++ b/localization/en_US/alert_for_admin_to_reset_pw.html
@@ -0,0 +1,2 @@
+ User [USER] requested a password change but they do not have an phone and alternate email address registered. Please change the password or register an phone and/or alternative email for this user. Hello! You have requested password recovery. Confirmation code: [CODE] Пользователь [USER] запросил(а) сброс пароля, но у него/неё не найдены параметры для восстановления пароля. Пожалуйста, поменяйте пароль пользователю или настройте дополнительный адрес электронной почты и/или телефон для него/неё. Здравствуйте! Вы запросили восстановление пароля. Код подтверждения: [CODE]