Просмотр исходного кода

Provide email address verification feature (#2481)

* Add an email field to the profile page

I reuse the `mail_login` from the configuration. I'm not sure if it's
useful today (I would say it was used when Persona login was available).

A good improvement would be to rename `mail_login` into `email` so it
would be more intuitive to use.

* Add boolean to the conf to force email validation

This commit only adds a configuration item.

* Add email during registration if email must be validated

* Set email token to validate when email changes

* Block access to FreshRSS if email is not validated

* Send email when address is changed

* Allow to resend the validation email

* Allow the user to change its email while blocked

* Document the email validation feature

* fixup! Allow the user to change its email while blocked

* tec: Autoload PHPMailer lib

* Validate email address format

* Add feedback on validation email resend action

* Allow to logout when user is blocked

* fix: Change default email "from"

* Reorganize i18n keys

* Complete all the locales with default english

* Hide sidebar (profile page) if email is not validated

* Check email requirements on registration

* Allow admin to specify email when creating users

* Don't check email format if value is empty

* Remove trailing comma in userController

Co-Authored-By: Alexandre Alapetite <alexandre@alapetite.fr>

* Set PHPMailer validator to html5 before sending email

* fixup! Remove trailing comma in userController
Marien Fressinaud 6 лет назад
Родитель
Сommit
75632e70f0
81 измененных файлов с 1002 добавлено и 18 удалено
  1. 1 0
      app/Controllers/authController.php
  2. 12 0
      app/Controllers/configureController.php
  3. 187 8
      app/Controllers/userController.php
  4. 18 0
      app/FreshRSS.php
  5. 31 0
      app/Mailers/UserMailer.php
  6. 4 0
      app/Models/ConfigurationSetter.php
  7. 1 0
      app/i18n/cz/admin.php
  8. 1 0
      app/i18n/cz/conf.php
  9. 1 0
      app/i18n/cz/gen.php
  10. 32 0
      app/i18n/cz/user.php
  11. 1 0
      app/i18n/de/admin.php
  12. 1 0
      app/i18n/de/conf.php
  13. 1 0
      app/i18n/de/gen.php
  14. 32 0
      app/i18n/de/user.php
  15. 1 0
      app/i18n/en/admin.php
  16. 1 0
      app/i18n/en/conf.php
  17. 1 0
      app/i18n/en/gen.php
  18. 32 0
      app/i18n/en/user.php
  19. 1 0
      app/i18n/es/admin.php
  20. 1 0
      app/i18n/es/conf.php
  21. 1 0
      app/i18n/es/gen.php
  22. 32 0
      app/i18n/es/user.php
  23. 1 0
      app/i18n/fr/admin.php
  24. 1 0
      app/i18n/fr/conf.php
  25. 1 0
      app/i18n/fr/gen.php
  26. 32 0
      app/i18n/fr/user.php
  27. 1 0
      app/i18n/he/admin.php
  28. 1 0
      app/i18n/he/conf.php
  29. 1 0
      app/i18n/he/gen.php
  30. 32 0
      app/i18n/he/user.php
  31. 1 0
      app/i18n/it/admin.php
  32. 1 0
      app/i18n/it/conf.php
  33. 1 0
      app/i18n/it/gen.php
  34. 32 0
      app/i18n/it/user.php
  35. 1 0
      app/i18n/kr/admin.php
  36. 1 0
      app/i18n/kr/conf.php
  37. 1 0
      app/i18n/kr/gen.php
  38. 32 0
      app/i18n/kr/user.php
  39. 1 0
      app/i18n/nl/admin.php
  40. 1 0
      app/i18n/nl/conf.php
  41. 1 0
      app/i18n/nl/gen.php
  42. 32 0
      app/i18n/nl/user.php
  43. 1 0
      app/i18n/oc/admin.php
  44. 1 0
      app/i18n/oc/conf.php
  45. 1 0
      app/i18n/oc/gen.php
  46. 32 0
      app/i18n/oc/user.php
  47. 1 0
      app/i18n/pt-br/admin.php
  48. 1 0
      app/i18n/pt-br/conf.php
  49. 1 0
      app/i18n/pt-br/gen.php
  50. 32 0
      app/i18n/pt-br/user.php
  51. 1 0
      app/i18n/ru/admin.php
  52. 1 0
      app/i18n/ru/conf.php
  53. 1 0
      app/i18n/ru/gen.php
  54. 32 0
      app/i18n/ru/user.php
  55. 1 0
      app/i18n/tr/admin.php
  56. 1 0
      app/i18n/tr/conf.php
  57. 1 0
      app/i18n/tr/gen.php
  58. 32 0
      app/i18n/tr/user.php
  59. 1 0
      app/i18n/zh-cn/admin.php
  60. 1 0
      app/i18n/zh-cn/conf.php
  61. 1 0
      app/i18n/zh-cn/gen.php
  62. 32 0
      app/i18n/zh-cn/user.php
  63. 66 0
      app/layout/simple.phtml
  64. 9 0
      app/views/auth/register.phtml
  65. 19 1
      app/views/configure/system.phtml
  66. 11 0
      app/views/user/manage.phtml
  67. 12 1
      app/views/user/profile.phtml
  68. 22 0
      app/views/user/validateEmail.phtml
  69. 5 0
      app/views/user_mailer/email_need_validation.txt
  70. 5 2
      cli/create-user.php
  71. 1 0
      cli/update-user.php
  72. 1 0
      config-user.default.php
  73. 8 1
      config.default.php
  74. 2 1
      docs/en/admins/01_Index.md
  75. 73 0
      docs/en/admins/05_Configuring_email_validation.md
  76. 2 4
      lib/Minz/Mailer.php
  77. 7 0
      lib/Minz/Request.php
  78. 0 0
      lib/PHPMailer/PHPMailer/Exception.php
  79. 0 0
      lib/PHPMailer/PHPMailer/PHPMailer.php
  80. 0 0
      lib/PHPMailer/PHPMailer/SMTP.php
  81. 16 0
      lib/lib_rss.php

+ 1 - 0
app/Controllers/authController.php

@@ -205,6 +205,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 			Minz_Error::error(403);
 		}
 
+		$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
 		Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
 	}
 }

+ 12 - 0
app/Controllers/configureController.php

@@ -293,15 +293,24 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 * configuration values then sends a notification to the user.
 	 *
 	 * The options available on the page are:
+	 *   - instance name (default: FreshRSS)
+	 *   - auto update URL (default: false)
+	 *   - force emails validation (default: false)
 	 *   - user limit (default: 1)
 	 *   - user category limit (default: 16384)
 	 *   - user feed limit (default: 16384)
 	 *   - user login duration for form auth (default: 2592000)
+	 *
+	 * The `force-email-validation` is ignored with PHP < 5.5
 	 */
 	public function systemAction() {
 		if (!FreshRSS_Auth::hasAccess('admin')) {
 			Minz_Error::error(403);
 		}
+
+		$can_enable_email_validation = version_compare(PHP_VERSION, '5.5') >= 0;
+		$this->view->can_enable_email_validation = $can_enable_email_validation;
+
 		if (Minz_Request::isPost()) {
 			$limits = FreshRSS_Context::$system_conf->limits;
 			$limits['max_registrations'] = Minz_Request::param('max-registrations', 1);
@@ -311,6 +320,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			FreshRSS_Context::$system_conf->limits = $limits;
 			FreshRSS_Context::$system_conf->title = Minz_Request::param('instance-name', 'FreshRSS');
 			FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::param('auto-update-url', false);
+			if ($can_enable_email_validation) {
+				FreshRSS_Context::$system_conf->force_email_validation = Minz_Request::param('force-email-validation', false);
+			}
 			FreshRSS_Context::$system_conf->save();
 
 			invalidateHttpCache();

+ 187 - 8
app/Controllers/userController.php

@@ -33,12 +33,23 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		return false;
 	}
 
-	public static function updateUser($user, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) {
+	public static function updateUser($user, $email, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) {
 		$userConfig = get_user_configuration($user);
 		if ($userConfig === null) {
 			return false;
 		}
 
+		if ($email !== null && $userConfig->mail_login !== $email) {
+			$userConfig->mail_login = $email;
+
+			if (FreshRSS_Context::$system_conf->force_email_validation) {
+				$salt = FreshRSS_Context::$system_conf->salt;
+				$userConfig->email_validation_token = sha1($salt . uniqid(mt_rand(), true));
+				$mailer = new FreshRSS_User_Mailer();
+				$mailer->send_email_need_validation($user, $userConfig);
+			}
+		}
+
 		if ($passwordPlain != '') {
 			$passwordHash = self::hashPassword($passwordPlain);
 			$userConfig->passwordHash = $passwordHash;
@@ -84,7 +95,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			$apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 
 			$username = Minz_Request::param('username');
-			$ok = self::updateUser($username, $passwordPlain, $apiPasswordPlain, array(
+			$ok = self::updateUser($username, null, $passwordPlain, $apiPasswordPlain, array(
 				'token' => Minz_Request::param('token', null),
 			));
 
@@ -111,25 +122,58 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			Minz_Error::error(403);
 		}
 
+		$email_not_verified = FreshRSS_Context::$user_conf->email_validation_token !== '';
+		if ($email_not_verified) {
+			$this->view->_layout('simple');
+			$this->view->disable_aside = true;
+		}
+
 		Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
 
 		Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
 
 		if (Minz_Request::isPost()) {
+			$system_conf = FreshRSS_Context::$system_conf;
+			$user_config = FreshRSS_Context::$user_conf;
+			$old_email = $user_config->mail_login;
+
+			$email = trim(Minz_Request::param('email', ''));
 			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
 			Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
 			$_POST['newPasswordPlain'] = '';
 
 			$apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true);
 
-			$ok = self::updateUser(Minz_Session::param('currentUser'), $passwordPlain, $apiPasswordPlain, array(
+			if ($system_conf->force_email_validation && empty($email)) {
+				Minz_Request::bad(
+					_t('user.email.feedback.required'),
+					array('c' => 'user', 'a' => 'profile')
+				);
+			}
+
+			if (!empty($email) && !validateEmailAddress($email)) {
+				Minz_Request::bad(
+					_t('user.email.feedback.invalid'),
+					array('c' => 'user', 'a' => 'profile')
+				);
+			}
+
+			$ok = self::updateUser(
+				Minz_Session::param('currentUser'),
+				$email,
+				$passwordPlain,
+				$apiPasswordPlain,
+				array(
 					'token' => Minz_Request::param('token', null),
-				));
+				)
+			);
 
 			Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
 
 			if ($ok) {
-				if ($passwordPlain == '') {
+				if ($system_conf->force_email_validation && $email !== $old_email) {
+					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'validateEmail'));
+				} elseif ($passwordPlain == '') {
 					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'profile'));
 				} else {
 					Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index'));
@@ -151,6 +195,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 
 		Minz_View::prependTitle(_t('admin.user.title') . ' · ');
 
+		$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
 		$this->view->current_user = Minz_Request::param('u');
 
 		$this->view->nb_articles = 0;
@@ -165,7 +210,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		}
 	}
 
-	public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) {
+	public static function createUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) {
 		if (!is_array($userConfig)) {
 			$userConfig = array();
 		}
@@ -193,7 +238,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		if ($ok) {
 			$userDAO = new FreshRSS_UserDAO();
 			$ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds);
-			$ok &= self::updateUser($new_user_name, $passwordPlain, $apiPasswordPlain);
+			$ok &= self::updateUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain);
 		}
 		return $ok;
 	}
@@ -204,6 +249,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * Request parameters are:
 	 *   - new_user_language
 	 *   - new_user_name
+	 *   - new_user_email
 	 *   - new_user_passwordPlain
 	 *   - r (i.e. a redirection url, optional)
 	 *
@@ -216,11 +262,28 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		}
 
 		if (Minz_Request::isPost()) {
+			$system_conf = FreshRSS_Context::$system_conf;
+
 			$new_user_name = Minz_Request::param('new_user_name');
+			$email = Minz_Request::param('new_user_email', '');
 			$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
 			$new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
 
-			$ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language));
+			if ($system_conf->force_email_validation && empty($email)) {
+				Minz_Request::bad(
+					_t('user.email.feedback.required'),
+					array('c' => 'auth', 'a' => 'register')
+				);
+			}
+
+			if (!empty($email) && !validateEmailAddress($email)) {
+				Minz_Request::bad(
+					_t('user.email.feedback.invalid'),
+					array('c' => 'auth', 'a' => 'register')
+				);
+			}
+
+			$ok = self::createUser($new_user_name, $email, $passwordPlain, '', array('language' => $new_user_language));
 			Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
 			$_POST['new_user_passwordPlain'] = '';
 			invalidateHttpCache();
@@ -272,6 +335,122 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		return $ok;
 	}
 
+	/**
+	 * This action validates an email address, based on the token sent by email.
+	 * It also serves the main page when user is blocked.
+	 *
+	 * Request parameters are:
+	 *   - username
+	 *   - token
+	 *
+	 * This route works with GET requests since the URL is provided by email.
+	 * The security risks (e.g. forged URL by an attacker) are not very high so
+	 * it's ok.
+	 *
+	 * It returns 404 error if `force_email_validation` is disabled or if the
+	 * user doesn't exist.
+	 *
+	 * It returns 403 if user isn't logged in and `username` param isn't passed.
+	 */
+	public function validateEmailAction() {
+		if (!FreshRSS_Context::$system_conf->force_email_validation) {
+			Minz_Error::error(404);
+		}
+
+		Minz_View::prependTitle(_t('user.email.validation.title') . ' · ');
+		$this->view->_layout('simple');
+
+		$username = Minz_Request::param('username');
+		$token = Minz_Request::param('token');
+
+		if ($username) {
+			$user_config = get_user_configuration($username);
+		} elseif (FreshRSS_Auth::hasAccess()) {
+			$user_config = FreshRSS_Context::$user_conf;
+		} else {
+			Minz_Error::error(403);
+		}
+
+		if (!FreshRSS_UserDAO::exists($username) || $user_config === null) {
+			Minz_Error::error(404);
+		}
+
+		if ($user_config->email_validation_token === '') {
+			Minz_Request::good(
+				_t('user.email.validation.feedback.unnecessary'),
+				array('c' => 'index', 'a' => 'index')
+			);
+		}
+
+		if ($token) {
+			if ($user_config->email_validation_token !== $token) {
+				Minz_Request::bad(
+					_t('user.email.validation.feedback.wrong_token'),
+					array('c' => 'user', 'a' => 'validateEmail')
+				);
+			}
+
+			$user_config->email_validation_token = '';
+			if ($user_config->save()) {
+				Minz_Request::good(
+					_t('user.email.validation.feedback.ok'),
+					array('c' => 'index', 'a' => 'index')
+				);
+			} else {
+				Minz_Request::bad(
+					_t('user.email.validation.feedback.error'),
+					array('c' => 'user', 'a' => 'validateEmail')
+				);
+			}
+		}
+	}
+
+	/**
+	 * This action resends a validation email to the current user.
+	 *
+	 * It only acts on POST requests but doesn't require any param (except the
+	 * CSRF token).
+	 *
+	 * It returns 403 error if the user is not logged in or 404 if request is
+	 * not POST. Else it redirects silently to the index if user has already
+	 * validated its email, or to the user#validateEmail route.
+	 */
+	public function sendValidationEmailAction() {
+		if (!FreshRSS_Auth::hasAccess()) {
+			Minz_Error::error(403);
+		}
+
+		if (!Minz_Request::isPost()) {
+			Minz_Error::error(404);
+		}
+
+		$username = Minz_Session::param('currentUser', '_');
+		$user_config = FreshRSS_Context::$user_conf;
+
+		if ($user_config->email_validation_token === '') {
+			Minz_Request::forward(array(
+				'c' => 'index',
+				'a' => 'index',
+			), true);
+		}
+
+		$mailer = new FreshRSS_User_Mailer();
+		$ok = $mailer->send_email_need_validation($username, $user_config);
+
+		$redirect_url = array('c' => 'user', 'a' => 'validateEmail');
+		if ($ok) {
+			Minz_Request::good(
+				_t('user.email.validation.feedback.email_sent'),
+				$redirect_url
+			);
+		} else {
+			Minz_Request::bad(
+				_t('user.email.validation.feedback.email_failed'),
+				$redirect_url
+			);
+		}
+	}
+
 	/**
 	 * This action delete an existing user.
 	 *

+ 18 - 0
app/FreshRSS.php

@@ -54,6 +54,8 @@ class FreshRSS extends Minz_FrontController {
 			Minz_ExtensionManager::enableByList($ext_list);
 		}
 
+		self::checkEmailValidated();
+
 		Minz_ExtensionManager::callHook('freshrss_init');
 	}
 
@@ -144,4 +146,20 @@ class FreshRSS extends Minz_FrontController {
 		FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
 		self::loadStylesAndScripts();
 	}
+
+	private static function checkEmailValidated() {
+		$email_not_verified = FreshRSS_Auth::hasAccess() && FreshRSS_Context::$user_conf->email_validation_token !== '';
+		$action_is_allowed = (
+			Minz_Request::is('user', 'validateEmail') ||
+			Minz_Request::is('user', 'sendValidationEmail') ||
+			Minz_Request::is('user', 'profile') ||
+			Minz_Request::is('auth', 'logout')
+		);
+		if ($email_not_verified && !$action_is_allowed) {
+			Minz_Request::forward(array(
+				'c' => 'user',
+				'a' => 'validateEmail',
+			), true);
+		}
+	}
 }

+ 31 - 0
app/Mailers/UserMailer.php

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * Manage the emails sent to the users.
+ */
+class FreshRSS_User_Mailer extends Minz_Mailer {
+	public function send_email_need_validation($username, $user_config) {
+		$this->view->_path('user_mailer/email_need_validation.txt');
+
+		$this->view->username = $username;
+		$this->view->site_title = FreshRSS_Context::$system_conf->title;
+		$this->view->validation_url = Minz_Url::display(
+			array(
+				'c' => 'user',
+				'a' => 'validateEmail',
+				'params' => array(
+					'username' => $username,
+					'token' => $user_config->email_validation_token
+				)
+			),
+			'txt',
+			true
+		);
+
+		$subject_prefix = '[' . FreshRSS_Context::$system_conf->title . ']';
+		return $this->mail(
+			$user_config->mail_login,
+			$subject_prefix . ' ' ._t('user.mailer.email_need_validation.title')
+		);
+	}
+}

+ 4 - 0
app/Models/ConfigurationSetter.php

@@ -389,4 +389,8 @@ class FreshRSS_ConfigurationSetter {
 
 		$data['auto_update_url'] = $value;
 	}
+
+	private function _force_email_validation(&$data, $value) {
+		$data['force_email_validation'] = $this->handleBool($value);
+	}
 }

+ 1 - 0
app/i18n/cz/admin.php

@@ -163,6 +163,7 @@ return array(
 			'help' => 'in seconds', //TODO - Translation
 			'number' => 'Duration to keep logged in', //TODO - Translation
 		),
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Instance name', //TODO - Translation
 		'max-categories' => 'Categories per user limit', //TODO - Translation
 		'max-feeds' => 'Feeds per user limit', //TODO - Translation

+ 1 - 0
app/i18n/cz/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Smazání účtu',
 			'warn' => 'Váš účet bude smazán spolu se všemi souvisejícími daty',
 		),
+		'email' => 'Email',
 		'password_api' => 'Password API<br /><small>(tzn. pro mobilní aplikace)</small>',
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
 		'password_format' => 'Alespoň 7 znaků',

+ 1 - 0
app/i18n/cz/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Aktualizovat',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Zpět na seznam RSS kanálů',
 		'cancel' => 'Zrušit',
 		'create' => 'Vytvořit',

+ 32 - 0
app/i18n/cz/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/de/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'Systemeinstellungen',
 		'auto-update-url' => 'Auto-update URL',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Dein Reader Name',
 		'max-categories' => 'Anzahl erlaubter Kategorien pro Benutzer',
 		'max-feeds' => 'Anzahl erlaubter Feeds pro Benutzer',

+ 1 - 0
app/i18n/de/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Accountlöschung',
 			'warn' => 'Dein Account und alle damit bezogenen Daten werden gelöscht.',
 		),
+		'email' => 'E-Mail-Adresse',
 		'password_api' => 'Passwort-API<br /><small>(z. B. für mobile Anwendungen)</small>',
 		'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
 		'password_format' => 'mindestens 7 Zeichen',

+ 1 - 0
app/i18n/de/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Aktualisieren',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Zurück zu Ihren RSS-Feeds gehen',
 		'cancel' => 'Abbrechen',
 		'create' => 'Erstellen',

+ 32 - 0
app/i18n/de/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/en/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'System configuration',
 		'auto-update-url' => 'Auto-update server URL',
+		'force_email_validation' => 'Force email addresses validation',
 		'instance-name' => 'Instance name',
 		'max-categories' => 'Categories per user limit',
 		'max-feeds' => 'Feeds per user limit',

+ 1 - 0
app/i18n/en/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Account deletion',
 			'warn' => 'Your account and all related data will be deleted.',
 		),
+		'email' => 'Email address',
 		'password_api' => 'API password<br /><small>(e.g., for mobile apps)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_format' => 'At least 7 characters',

+ 1 - 0
app/i18n/en/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Actualize',
+		'back' => '← Go back',
 		'back_to_rss_feeds' => '← Go back to your RSS feeds',
 		'cancel' => 'Cancel',
 		'create' => 'Create',

+ 32 - 0
app/i18n/en/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.',
+			'required' => 'The email address is required.',
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.',
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.',
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.',
+				'email_sent' => 'An email has been sent to your address.',
+				'error' => 'The email address failed to be validated.',
+				'ok' => 'The email address has been validated.',
+				'unneccessary' => 'The email address was already validated.',
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.',
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.',
+			'resend_email' => 'Resend the email',
+			'title' => 'Email address validation',
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account',
+			'welcome' => 'Welcome %s,',
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:',
+		),
+	),
+);

+ 1 - 0
app/i18n/es/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'Configuración del sistema',
 		'auto-update-url' => 'URL de auto-actualización',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Nombre de la fuente',
 		'max-categories' => 'Límite de categorías por usuario',
 		'max-feeds' => 'Límite de fuentes por usuario',

+ 1 - 0
app/i18n/es/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Borrar cuenta',
 			'warn' => 'Tu cuenta y todos los datos asociados serán eliminados.',
 		),
+		'email' => 'Correo electrónico',
 		'password_api' => 'Contraseña API <br /><small>(para apps móviles, por ej.)</small>',
 		'password_form' => 'Contraseña<br /><small>(para el método de identificación por formulario web)</small>',
 		'password_format' => 'Mínimo de 7 caracteres',

+ 1 - 0
app/i18n/es/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Actualizar',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← regresar a tus fuentes RSS',
 		'cancel' => 'Cancelar',
 		'create' => 'Crear',

+ 32 - 0
app/i18n/es/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/fr/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'Configuration du système',
 		'auto-update-url' => 'URL du service de mise à jour',
+		'force_email_validation' => 'Forcer la validation des adresses email',
 		'instance-name' => 'Nom de l’instance',
 		'max-categories' => 'Limite de catégories par utilisateur',
 		'max-feeds' => 'Limite de flux par utilisateur',

+ 1 - 0
app/i18n/fr/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Suppression du compte',
 			'warn' => 'Le compte et toutes les données associées vont être supprimées.',
 		),
+		'email' => 'Adresse email',
 		'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
 		'password_format' => '7 caractères minimum',

+ 1 - 0
app/i18n/fr/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Actualiser',
+		'back' => '← Retour',
 		'back_to_rss_feeds' => '← Retour à vos flux RSS',
 		'cancel' => 'Annuler',
 		'create' => 'Créer',

+ 32 - 0
app/i18n/fr/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'L’adresse email est invalide.',
+			'required' => 'L’adresse email est requise.',
+		),
+		'validation' => array(
+			'change_email' => 'Vous pouvez changer votre adresse email <a href="%s">dans votre profil</a>.',
+			'email_sent_to' => 'Nous venons d’envoyer un email à <strong>%s</strong>, veuillez suivre ses indications pour valider votre adresse.',
+			'feedback' => array(
+				'email_failed' => 'Nous n’avons pas pu vous envoyer d’email à cause d’une mauvaise configuration du serveur.',
+				'email_sent' => 'Un email a été envoyé à votre adresse.',
+				'error' => 'L’adresse email n’a pas pu être validée.',
+				'ok' => 'L’adresse email a été validée.',
+				'unnecessary' => 'L’adresse email a déjà été validée.',
+				'wrong_token' => 'L’adresse email n’a pas pu être validée à cause d’un mauvais token.',
+			),
+			'need_to' => 'Vous devez valider votre adresse email avant de pouvoir utiliser %s.',
+			'resend_email' => 'Renvoyer l’email',
+			'title' => 'Validation de l’adresse email',
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'Vous devez valider votre compte',
+			'welcome' => 'Bienvenue %s,',
+			'body' => 'Vous venez de vous inscrire sur %s mais vous devez encore valider votre adresse email. Pour cela, veuillez cliquer sur ce lien :',
+		),
+	),
+);

+ 1 - 0
app/i18n/he/admin.php

@@ -163,6 +163,7 @@ return array(
 			'help' => 'in seconds', //TODO - Translation
 			'number' => 'Duration to keep logged in', //TODO - Translation
 		),
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Instance name', //TODO - Translation
 		'max-categories' => 'Categories per user limit', //TODO - Translation
 		'max-feeds' => 'Feeds per user limit', //TODO - Translation

+ 1 - 0
app/i18n/he/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Account deletion',	//TODO - Translation
 			'warn' => 'Your account and all related data will be deleted.',	//TODO - Translation
 		),
+		'email' => 'Email address',	//TODO - Translation
 		'password_api' => 'סיסמת API<br /><small>(לדוגמה ליישומים סלולריים)</small>',
 		'password_form' => 'סיסמה<br /><small>(לשימוש בטפוס ההרשמה)</small>',
 		'password_format' => 'At least 7 characters',	//TODO - Translation

+ 1 - 0
app/i18n/he/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'מימוש',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← חזרה להזנות הRSS שלך',
 		'cancel' => 'ביטול',
 		'create' => 'יצירה',

+ 32 - 0
app/i18n/he/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/it/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'Configurazione di sistema',
 		'auto-update-url' => 'Auto-update server URL',	//TODO - Translation
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Nome istanza',
 		'max-categories' => 'Limite categorie per utente',
 		'max-feeds' => 'Limite feeds per utente',

+ 1 - 0
app/i18n/it/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Cancellazione account',
 			'warn' => 'Il tuo account e tutti i dati associati saranno cancellati.',
 		),
+		'email' => 'Indirizzo email',
 		'password_api' => 'Password API<br /><small>(e.g., per applicazioni mobili)</small>',
 		'password_form' => 'Password<br /><small>(per il login classico)</small>',
 		'password_format' => 'Almeno 7 caratteri',

+ 1 - 0
app/i18n/it/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Aggiorna',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Indietro',
 		'cancel' => 'Annulla',
 		'create' => 'Crea',

+ 32 - 0
app/i18n/it/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/kr/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => '시스템 설정',
 		'auto-update-url' => '자동 업데이트 서버 URL',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => '인스턴스 이름',
 		'max-categories' => '사용자별 카테고리 개수 제한',
 		'max-feeds' => '사용자별 피드 개수 제한',

+ 1 - 0
app/i18n/kr/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => '계정 삭제',
 			'warn' => '당신의 계정과 관련된 모든 데이터가 삭제됩니다.',
 		),
+		'email' => '메일 주소',
 		'password_api' => 'API 암호<br /><small>(예: 모바일 애플리케이션)</small>',
 		'password_form' => '암호<br /><small>(웹폼 로그인 방식 사용시)</small>',
 		'password_format' => '7 글자 이상이어야 합니다',

+ 1 - 0
app/i18n/kr/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => '새 글 가져오기',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← RSS 피드로 돌아가기',
 		'cancel' => '취소',
 		'create' => '생성',

+ 32 - 0
app/i18n/kr/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/nl/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'Systeem configuratie',
 		'auto-update-url' => 'Automatische update server URL',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Voorbeeld naam',
 		'max-categories' => 'Categorielimiet per gebruiker',
 		'max-feeds' => 'Feedlimiet per gebruiker',

+ 1 - 0
app/i18n/nl/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Account verwijderen',
 			'warn' => 'Uw account en alle gerelateerde gegvens worden verwijderd.',
 		),
+		'email' => 'Email adres',
 		'password_api' => 'Wachtwoord API<br /><small>(e.g., voor mobiele apps)</small>',
 		'password_form' => 'Wachtwoord<br /><small>(voor de Web-formulier log in methode)</small>',
 		'password_format' => 'Ten minste 7 tekens',

+ 1 - 0
app/i18n/nl/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Actualiseren',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Ga terug naar je RSS feeds',
 		'cancel' => 'Annuleren',
 		'create' => 'Opslaan',

+ 32 - 0
app/i18n/nl/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/oc/admin.php

@@ -163,6 +163,7 @@ return array(
 			'help' => 'en segondas',
 			'number' => 'Durada de téner d’ésser connectat',
 		),
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Nom de l’instància',
 		'max-categories' => 'Limita de categoria per utilizaire',
 		'max-feeds' => 'Limita de fluxes per utilizaire',

+ 1 - 0
app/i18n/oc/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Supression del compte',
 			'warn' => 'Lo compte e totas las donadas ligadas seràn suprimits.',
 		),
+		'email' => 'Adreça de corrièl',
 		'password_api' => 'Senhal API<br /><small>(ex. : per las aplicacions mobil)</small>',
 		'password_form' => 'Senhal API<br /><small>(ex. : per la connexion via formulari)</small>',
 		'password_format' => 'Almens 7 caractèrs',

+ 1 - 0
app/i18n/oc/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Actualizar',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Tornar a vòstres fluxes RSS',
 		'cancel' => 'Anullar',
 		'create' => 'Crear',

+ 32 - 0
app/i18n/oc/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/pt-br/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'Configuração do sistema',
 		'auto-update-url' => 'URL do servidor para atualização automática',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Nome da instância',
 		'max-categories' => 'Limite de categorias por usuário',
 		'max-feeds' => 'Limite de Feeds por usuário',

+ 1 - 0
app/i18n/pt-br/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Remover conta',
 			'warn' => 'Sua conta e todos os dados relacionados serão removidos.',
 		),
+		'email' => 'Endereço de e-mail',
 		'password_api' => 'Senha da API<br /><small>(p.s., para aplicativos móveis)</small>',
 		'password_form' => 'Senha<br /><small>(para o método de formulário web)</small>',
 		'password_format' => 'Ao menos 7 caracteres',

+ 1 - 0
app/i18n/pt-br/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Atualizar',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Volte para o seu feeds RSS',
 		'cancel' => 'Cancelar',
 		'create' => 'Criar',

+ 32 - 0
app/i18n/pt-br/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/ru/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'Системные настройки',
 		'auto-update-url' => 'Адрес сервера для автоматического обновления',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Название этого сервера',
 		'max-categories' => 'Количество категорий на пользователя',
 		'max-feeds' => 'Количество статей на пользователя',

+ 1 - 0
app/i18n/ru/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Account deletion',	//TODO - Translation
 			'warn' => 'Your account and all the related data will be deleted.',	//TODO - Translation
 		),
+		'email' => 'Email address',	//TODO - Translation
 		'password_api' => 'Password API<br /><small>(e.g., for mobile apps)</small>',	//TODO - Translation
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',	//TODO - Translation
 		'password_format' => 'At least 7 characters',	//TODO - Translation

+ 1 - 0
app/i18n/ru/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Actualize',	//TODO - Translation
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← Go back to your RSS feeds',	//TODO - Translation
 		'cancel' => 'Cancel',	//TODO - Translation
 		'create' => 'Create',	//TODO - Translation

+ 32 - 0
app/i18n/ru/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/tr/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => 'Sistem yapılandırması',
 		'auto-update-url' => 'Otomatik güncelleme sunucu URL',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => 'Örnek isim',
 		'max-categories' => 'Kullanıcı başına kategori limiti',
 		'max-feeds' => 'Kullanıcı başına akış limiti',

+ 1 - 0
app/i18n/tr/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => 'Hesap silme',
 			'warn' => 'Hesabınız ve tüm verileriniz silinecek.',
 		),
+		'email' => 'Email adresleri',
 		'password_api' => 'API Şifresi<br /><small>(ör. mobil uygulamalar için)</small>',
 		'password_form' => 'Şifre<br /><small>(Tarayıcı girişi için)</small>',
 		'password_format' => 'En az 7 karakter',

+ 1 - 0
app/i18n/tr/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => 'Yenile',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← RSS akışlarınız için geri gidin',
 		'cancel' => 'İptal',
 		'create' => 'Oluştur',

+ 32 - 0
app/i18n/tr/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 1 - 0
app/i18n/zh-cn/admin.php

@@ -159,6 +159,7 @@ return array(
 	'system' => array(
 		'_' => '系统配置',
 		'auto-update-url' => '自动升级服务器 URL',
+		'force_email_validation' => 'Force email addresses validation', //TODO - Translation
 		'instance-name' => '实例名称',
 		'max-categories' => '每用户分类限制',
 		'max-feeds' => '每用户 RSS 源限制',

+ 1 - 0
app/i18n/zh-cn/conf.php

@@ -46,6 +46,7 @@ return array(
 			'_' => '账户删除',
 			'warn' => '你的帐户和所有相关数据都将被删除。',
 		),
+		'email' => 'Email 地址',
 		'password_api' => 'API 密码<br /><small>(例如,用于手机 APP)</small>',
 		'password_form' => '密码<br /><small>(用于 Web-form 登录方式)</small>',
 		'password_format' => '至少 7 个字符',

+ 1 - 0
app/i18n/zh-cn/gen.php

@@ -3,6 +3,7 @@
 return array(
 	'action' => array(
 		'actualize' => '获取',
+		'back' => '← Go back', //TODO - Translation
 		'back_to_rss_feeds' => '← 返回',
 		'cancel' => '取消',
 		'create' => '创建',

+ 32 - 0
app/i18n/zh-cn/user.php

@@ -0,0 +1,32 @@
+<?php
+
+return array(
+	'email' => array(
+		'feedback' => array(
+			'invalid' => 'The email address is invalid.', //TODO - Translation
+			'required' => 'The email address is required.', //TODO - Translation
+		),
+		'validation' => array(
+			'change_email' => 'You can change your email address <a href="%s">on the profile page</a>.', //TODO - Translation
+			'email_sent_to' => 'We sent you an email at <strong>%s</strong>, please follow its indications to validate your address.', //TODO - Translation
+			'feedback' => array(
+				'email_failed' => 'We couldn’t send you an email because of a misconfiguration of the server.', //TODO - Translation
+				'email_sent' => 'An email has been sent to your address.', //TODO - Translation
+				'error' => 'The email address failed to be validated.', //TODO - Translation
+				'ok' => 'The email address has been validated.', //TODO - Translation
+				'unneccessary' => 'The email address was already validated.', //TODO - Translation
+				'wrong_token' => 'The email address failed to be validated due to a wrong token.', //TODO - Translation
+			),
+			'need_to' => 'You need to validate your email address before being able to use %s.', //TODO - Translation
+			'resend_email' => 'Resend the email', //TODO - Translation
+			'title' => 'Email address validation', //TODO - Translation
+		),
+	),
+	'mailer' => array(
+		'email_need_validation' => array(
+			'title' => 'You need to validate your account', //TODO - Translation
+			'welcome' => 'Welcome %s,', //TODO - Translation
+			'body' => 'You’ve just registered on %s but you still need to validate your email. For that, just follow the link:', //TODO - Translation
+		),
+	),
+);

+ 66 - 0
app/layout/simple.phtml

@@ -0,0 +1,66 @@
+<?php FreshRSS::preLayout(); ?>
+<!DOCTYPE html>
+<html lang="<?php echo FreshRSS_Context::$user_conf->language; ?>" xml:lang="<?php echo FreshRSS_Context::$user_conf->language; ?>">
+	<head>
+		<meta charset="UTF-8" />
+		<meta name="viewport" content="initial-scale=1.0" />
+		<?php echo self::headStyle(); ?>
+		<?php echo self::headScript(); ?>
+		<link rel="shortcut icon" id="favicon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" />
+		<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?php echo Minz_Url::display('/themes/icons/favicon-256.png'); ?>" />
+		<link rel="apple-touch-icon" href="<?php echo Minz_Url::display('/themes/icons/apple-touch-icon.png'); ?>" />
+		<meta name="apple-mobile-web-app-capable" content="yes" />
+		<meta name="apple-mobile-web-app-status-bar-style" content="black" />
+		<meta name="apple-mobile-web-app-title" content="<?php echo FreshRSS_Context::$system_conf->title; ?>">
+		<meta name="msapplication-TileColor" content="#FFF" />
+		<meta name="referrer" content="never" />
+		<meta name="robots" content="noindex,nofollow" />
+		<?php echo self::headTitle(); ?>
+	</head>
+	<body>
+
+<?php flush(); ?>
+<div class="app-layout app-layout-simple">
+	<div class="header">
+		<div class="item title">
+			<h1>
+				<a href="<?php echo _url('index', 'index'); ?>">
+					<img class="logo" src="<?php echo _i('icon', true); ?>" alt="" />
+					<?php echo FreshRSS_Context::$system_conf->title; ?>
+				</a>
+			</h1>
+		</div>
+
+		<div class="item"></div>
+
+		<div class="item">
+			<?php if (FreshRSS_Auth::accessNeedsAction()) { ?>
+				<a class="signout" href="<?php echo _url('auth', 'logout'); ?>">
+					<?php echo _i('logout') . _t('gen.auth.logout'); ?>
+
+					(<?php echo htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8'); ?>)
+				</a>
+			<?php } ?>
+		</div>
+	</div>
+
+	<?php $this->render(); ?>
+</div>
+
+<?php
+	$msg = '';
+	$status = 'closed';
+	if (isset($this->notification)) {
+		$msg = $this->notification['content'];
+		$status = $this->notification['type'];
+
+		invalidateHttpCache();
+	}
+?>
+<div id="notification" class="notification <?php echo $status; ?>">
+	<span class="msg"><?php echo $msg; ?></span>
+	<a class="close" href=""><?php echo _i('close'); ?></a>
+</div>
+
+	</body>
+</html>

+ 9 - 0
app/views/auth/register.phtml

@@ -8,6 +8,15 @@
 			<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" autocomplete="off" pattern="<?php echo FreshRSS_user_Controller::USERNAME_PATTERN; ?>" />
 		</div>
 
+		<?php if ($this->show_email_field) { ?>
+			<div>
+				<label class="group-name" for="new_user_email">
+					<?php echo _t('gen.auth.email'); ?>
+				</label>
+				<input id="new_user_email" name="new_user_email" type="email" required />
+			</div>
+		<?php } ?>
+
 		<div>
 			<label class="group-name" for="new_user_passwordPlain"><?php echo _t('gen.auth.password'), '<br />', _i('help'), ' ', _t('gen.auth.password.format'); ?></label>
 			<div class="stick">

+ 19 - 1
app/views/configure/system.phtml

@@ -38,6 +38,24 @@
 			</div>
 		</div>
 
+		<?php if ($this->can_enable_email_validation) { ?>
+			<div class="form-group">
+				<div class="group-controls">
+					<label class="checkbox" for="force-email-validation">
+						<input
+							type="checkbox"
+							name="force-email-validation"
+							id="force-email-validation"
+							value="1"
+							<?php echo FreshRSS_Context::$system_conf->force_email_validation ? 'checked="checked"' : ''; ?>
+							data-leave-validation="<?php echo FreshRSS_Context::$system_conf->force_email_validation; ?>"
+						/>
+						<?php echo _t('admin.system.force_email_validation'); ?>
+					</label>
+				</div>
+			</div>
+		<?php } ?>
+
 		<div class="form-group">
 			<label class="group-name" for="max-feeds"><?php echo _t('admin.system.max-feeds'); ?></label>
 			<div class="group-controls">
@@ -51,7 +69,7 @@
 			    <input type="number" id="max-categories" name="max-categories" value="<?php echo FreshRSS_Context::$system_conf->limits['max_categories']; ?>" min="1" data-leave-validation="<?php echo FreshRSS_Context::$system_conf->limits['max_categories']; ?>"/>
 			</div>
 		</div>
-		
+
 		<div class="form-group">
 			<label class="group-name" for="cookie-duration"><?php echo _t('admin.system.cookie-duration.number'); ?></label>
 			<div class="group-controls">

+ 11 - 0
app/views/user/manage.phtml

@@ -26,6 +26,17 @@
 			</div>
 		</div>
 
+		<?php if ($this->show_email_field) { ?>
+			<div class="form-group">
+				<label class="group-name" for="new_user_email">
+					<?php echo _t('gen.auth.email'); ?>
+				</label>
+				<div class="group-controls">
+					<input id="new_user_email" name="new_user_email" type="email" required />
+				</div>
+			</div>
+		<?php } ?>
+
 		<div class="form-group">
 			<label class="group-name" for="new_user_passwordPlain"><?php echo _t('admin.user.password_form'); ?></label>
 			<div class="group-controls">

+ 12 - 1
app/views/user/profile.phtml

@@ -1,4 +1,8 @@
-<?php $this->partial('aside_configure'); ?>
+<?php
+    if (!$this->disable_aside) {
+        $this->partial('aside_configure');
+    }
+?>
 
 <div class="post">
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a>
@@ -18,6 +22,13 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<label class="group-name" for="email"><?php echo _t('conf.profile.email'); ?></label>
+			<div class="group-controls">
+				<input id="email" name="email" type="email" value="<?php echo FreshRSS_Context::$user_conf->mail_login; ?>" />
+			</div>
+		</div>
+
 		<div class="form-group">
 			<label class="group-name" for="newPasswordPlain"><?php echo _t('conf.profile.password_form'); ?></label>
 			<div class="group-controls">

+ 22 - 0
app/views/user/validateEmail.phtml

@@ -0,0 +1,22 @@
+<div class="post">
+    <p>
+        <?php echo _t('user.email.validation.need_to', FreshRSS_Context::$system_conf->title); ?>
+    </p>
+
+    <p>
+        <?php echo _t('user.email.validation.email_sent_to', FreshRSS_Context::$user_conf->mail_login); ?>
+    </p>
+
+    <form action="<?php echo _url('user', 'sendValidationEmail'); ?>" method="post">
+        <input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
+        <button type="submit" class="btn">
+            <?php echo _t('user.email.validation.resend_email'); ?>
+        </button>
+    </form>
+
+    <p>
+        <small>
+            <?php echo _t('user.email.validation.change_email', _url('user', 'profile')); ?>
+        </small>
+    </p>
+</div>

+ 5 - 0
app/views/user_mailer/email_need_validation.txt

@@ -0,0 +1,5 @@
+<?php echo _t('user.mailer.email_need_validation.welcome', $this->username); ?>
+
+<?php echo _t('user.mailer.email_need_validation.body', $this->site_title); ?>
+
+<?php echo $this->validation_url; ?>

+ 5 - 2
cli/create-user.php

@@ -16,11 +16,14 @@ if (preg_grep("/^$username$/i", $usernames)) {
 
 echo 'FreshRSS creating user “', $username, "”…\n";
 
-$ok = FreshRSS_user_Controller::createUser($username,
+$ok = FreshRSS_user_Controller::createUser(
+	$username,
+	empty($options['mail_login']) ? '' : $options['mail_login'],
 	empty($options['password']) ? '' : $options['password'],
 	empty($options['api_password']) ? '' : $options['api_password'],
 	$values,
-	!isset($options['no_default_feeds']));
+	!isset($options['no_default_feeds'])
+);
 
 if (!$ok) {
 	fail('FreshRSS could not create user!');

+ 1 - 0
cli/update-user.php

@@ -9,6 +9,7 @@ echo 'FreshRSS updating user “', $username, "”…\n";
 
 $ok = FreshRSS_user_Controller::updateUser(
 	$username,
+	empty($options['mail_login']) ? null : $options['mail_login'],
 	empty($options['password']) ? '' : $options['password'],
 	empty($options['api_password']) ? '' : $options['api_password'],
 	$values);

+ 1 - 0
config-user.default.php

@@ -6,6 +6,7 @@ return array (
 	'keep_history_default' => 50,
 	'ttl_default' => 3600,
 	'mail_login' => '',
+	'email_validation_token' => '',
 	'token' => '',
 	'passwordHash' => '',
 	'apiPasswordHash' => '',

+ 8 - 1
config.default.php

@@ -33,6 +33,13 @@ return array(
 	# Name of the user that has administration rights.
 	'default_user' => '_',
 
+	# Force users to validate their email address. If `true`, an email with a
+	# validation URL is sent during registration, and users cannot access their
+	# feed if they didn't access this URL.
+	# Note: it is recommended to not enable it with PHP < 5.5 (emails cannot be
+	# sent).
+	'force_email_validation' => false,
+
 	# Allow or not visitors without login to see the articles
 	#	of the default user.
 	'allow_anonymous' => false,
@@ -159,7 +166,7 @@ return array(
 		'username' => '',
 		'password' => '',
 		'secure' => '', // '', 'ssl' or 'tls'
-		'from' => 'noreply@localhost',
+		'from' => 'root@localhost',
 	),
 
 	# List of enabled FreshRSS extensions.

+ 2 - 1
docs/en/admins/01_Index.md

@@ -5,5 +5,6 @@ Learn how to install, update and backup FreshRSS and how to use the command line
 * [Install FreshRSS](02_Installation.md) on your server
 * [Update your installation](03_Updating.md) to the latest stable or dev version
 * [The command line interface](https://github.com/FreshRSS/FreshRSS/tree/master/cli) can be used to administrate feeds and users
-* [Automatic feed updates](https://github.com/FreshRSS/FreshRSS#automatic-feed-update) using cron is the preferred way to get the latest feeds entries   
+* [Automatic feed updates](https://github.com/FreshRSS/FreshRSS#automatic-feed-update) using cron is the preferred way to get the latest feeds entries
+* [Configuring the email address validation](05_Configuring_email_validation.md)
 * [Frequently asked questions](04_Frequently_Asked_Questions.md)

+ 73 - 0
docs/en/admins/05_Configuring_email_validation.md

@@ -0,0 +1,73 @@
+# Configuring the email address validation
+
+FreshRSS can verify that users give a valid email address. It is not configured
+by default so you'll have to follow these few steps to verify email addresses.
+
+It is intended to administrators who host users and want to be sure to be able
+to contact them.
+
+Note that this feature only works with PHP >= 5.5.
+
+## Force email validation
+
+In your `data/config.php` file, you'll find a `force_email_validation` item:
+set it to `true`. An email field now appears on the registration page and
+emails are sent when users change their email.
+
+You can also enable this feature directly in FreshRSS: `Administration` >
+`System configuration` > check `Force email addresses validation`. If the
+option doesn't appear, it means that you use PHP < 5.5.
+
+## Configure the SMTP server
+
+By default, FreshRSS will attempt to send emails with the [`mail`](https://www.php.net/manual/en/function.mail.php)
+function of PHP. It is the simpler solution but it might not work as expected.
+For example, we don't support (yet?) sending emails from inside our official
+Docker images. We recommend to use a proper SMTP server.
+
+To configure a SMTP server, you'll have to modify the `data/config.php` file.
+
+First, change the `mailer` item to `smtp` (instead of the default `mail`).
+
+Then, you should change the `smtp` options like you would do with a regular
+email client. You can find the full list of options in the [`config.default.php` file](/config.default.php).
+If you're not sure to what each item is corresponding, you may find useful [the
+PHPMailer documentation](http://phpmailer.github.io/PHPMailer/classes/PHPMailer.PHPMailer.PHPMailer.html#properties)
+(which is used by FreshRSS under the hood).
+
+## Check your SMTP server is correctly configured
+
+To do so, once you've enabled the `force_email_validation` option, you only
+need to change your email address on the profile page and check that an email
+arrives on the new address.
+
+If it fails, you can change the environment (in `data/config.php` file, change
+`production` to `development`). PHPMailer will become more verbose and you'll
+be able to see what happens in the PHP logs. If something's wrong here, you'll
+probably better served by asking to your favorite search engine than asking us.
+If you think that something's wrong in FreshRSS code, don't hesitate to open a
+ticket though.
+
+Also, make sure the email didn't arrive in your spam.
+
+Once you're done, don't forget to reconfigure your environment to `production`.
+
+## Access the validation URL during development
+
+You might find painful to configure a SMTP server when you're developping and
+`mail` function will not work on your local machine. For the moment, there is
+no easy way to access the validation URL unless forging it. You'll need to
+information:
+
+- the username of the user to validate (you should know it)
+- its validation token, that you'll find in its configuration file:
+
+```console
+$ # For instance, for a user called `alice`
+$ grep email_validation_token data/users/alice/config.php | cut -d \' -f 4 -
+3d75042a4471994a0346e18ae87602f19220a795
+```
+
+Then, the validation URL should be `http://localhost:8080/i/?c=user&a=validateEmail&username=alice&token=3d75042a4471994a0346e18ae87602f19220a795`
+
+Don't forget to adapt this URL with the correct port, username and token.

+ 2 - 4
lib/Minz/Mailer.php

@@ -3,10 +3,6 @@
 use PHPMailer\PHPMailer\PHPMailer;
 use PHPMailer\PHPMailer\Exception;
 
-require LIB_PATH . '/PHPMailer/PHPMailer.php';
-require LIB_PATH . '/PHPMailer/Exception.php';
-require LIB_PATH . '/PHPMailer/SMTP.php';
-
 /**
  * Allow to send emails.
  *
@@ -78,6 +74,8 @@ class Minz_Mailer {
 		$body = ob_get_contents();
 		ob_end_clean();
 
+		PHPMailer::$validator = 'html5';
+
 		$mail = new PHPMailer(true);
 		try {
 			// Server settings

+ 7 - 0
lib/Minz/Request.php

@@ -98,6 +98,13 @@ class Minz_Request {
 		self::initJSON();
 	}
 
+	public static function is($controller_name, $action_name) {
+		return (
+			self::$controller_name === $controller_name &&
+			self::$action_name === $action_name
+		);
+	}
+
 	/**
 	 * Return true if the request is over HTTPS, false otherwise (HTTP)
 	 */

+ 0 - 0
lib/PHPMailer/Exception.php → lib/PHPMailer/PHPMailer/Exception.php


+ 0 - 0
lib/PHPMailer/PHPMailer.php → lib/PHPMailer/PHPMailer/PHPMailer.php


+ 0 - 0
lib/PHPMailer/SMTP.php → lib/PHPMailer/PHPMailer/SMTP.php


+ 16 - 0
lib/lib_rss.php

@@ -55,6 +55,8 @@ function classAutoloader($class) {
 		include(LIB_PATH . '/' . str_replace('_', '/', $class) . '.php');
 	} elseif (strpos($class, 'SimplePie') === 0) {
 		include(LIB_PATH . '/SimplePie/' . str_replace('_', '/', $class) . '.php');
+	} elseif (strpos($class, 'PHPMailer') === 0) {
+		include(LIB_PATH . '/' . str_replace('\\', '/', $class) . '.php');
 	}
 }
 
@@ -277,6 +279,20 @@ function sanitizeHTML($data, $base = '') {
 	return html_only_entity_decode($simplePie->sanitize->sanitize($data, SIMPLEPIE_CONSTRUCT_HTML, $base));
 }
 
+/**
+ * Validate an email address, supports internationalized addresses.
+ *
+ * @param string $email The address to validate
+ *
+ * @return bool true if email is valid, else false
+ */
+function validateEmailAddress($email) {
+	$mailer = new PHPMailer\PHPMailer\PHPMailer();
+	$mailer->Charset = 'utf-8';
+	$punyemail = $mailer->punyencodeAddress($email);
+	return PHPMailer\PHPMailer\PHPMailer::validateAddress($punyemail, 'html5');
+}
+
 /**
  * Add support of image lazy loading
  * Move content from src attribute to data-original