diff --git a/cms/envs/common.py b/cms/envs/common.py index 32310f9c24..f6221f1590 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -276,6 +276,10 @@ FEATURES = { # Whether or not the dynamic EnrollmentTrackUserPartition should be registered. 'ENABLE_ENROLLMENT_TRACK_USER_PARTITION': True, + # Whether to send an email for failed password reset attempts or not. This is mainly useful for notifying users + # that they don't have an account associated with email addresses they believe they've registered with. + 'ENABLE_PASSWORD_RESET_FAILURE_EMAIL': False, + # Whether archived courses (courses with end dates in the past) should be # shown in Studio in a separate list. 'ENABLE_SEPARATE_ARCHIVED_COURSES': True, diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 37722ff1fc..f5f9a20e1a 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -51,6 +51,9 @@ from openedx.core.djangoapps.user_api.errors import UserAPIInternalError LOGGER_NAME = 'audit' User = get_user_model() # pylint:disable=invalid-name +FEATURES_WITH_FAILED_PASSWORD_RESET_EMAIL = settings.FEATURES.copy() +FEATURES_WITH_FAILED_PASSWORD_RESET_EMAIL['ENABLE_PASSWORD_RESET_FAILURE_EMAIL'] = True + @ddt.ddt class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin): @@ -140,6 +143,28 @@ class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin): self._change_password() self.assertRaises(UserAPIInternalError) + @override_settings(FEATURES=FEATURES_WITH_FAILED_PASSWORD_RESET_EMAIL) + def test_password_reset_failure_email(self): + """Test that a password reset failure email notification is sent, when enabled.""" + # Log the user out + self.client.logout() + + bad_email = 'doesnotexist@example.com' + response = self._change_password(email=bad_email) + self.assertEqual(response.status_code, 200) + + # Check that an email was sent + self.assertEqual(len(mail.outbox), 1) + + # Verify that the body contains the failed password reset message + email_body = mail.outbox[0].body + self.assertIn( + 'However, there is currently no user account associated with your email address: {email}'.format( + email=bad_email + ), + email_body, + ) + @ddt.data(True, False) def test_password_change_logged_out(self, send_email): # Log the user out diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 2358cf8edc..7d4b803011 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -9,9 +9,11 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden +from django.core.mail import send_mail +from django.core.urlresolvers import resolve, reverse +from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from django.shortcuts import redirect +from django.template import loader from django.utils.translation import ugettext as _ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_http_methods @@ -216,6 +218,25 @@ def password_change_request_handler(request): AUDIT_LOG.info("Invalid password reset attempt") # Increment the rate limit counter limiter.tick_bad_request_counter(request) + + # If enabled, send an email saying that a password reset was attempted, but that there is + # no user associated with the email + if configuration_helpers.get_value('ENABLE_PASSWORD_RESET_FAILURE_EMAIL', + settings.FEATURES['ENABLE_PASSWORD_RESET_FAILURE_EMAIL']): + context = { + 'failed': True, + 'email_address': email, + 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME), + + } + subject = loader.render_to_string('emails/password_reset_subject.txt', context) + subject = ''.join(subject.splitlines()) + message = loader.render_to_string('registration/password_reset_email.html', context) + from_email = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) + try: + send_mail(subject, message, from_email, [email]) + except Exception: # pylint: disable=broad-except + log.exception(u'Unable to send password reset failure email notification from "%s"', from_email) except UserAPIInternalError as err: log.exception('Error occured during password change for user {email}: {error}' .format(email=email, error=err)) diff --git a/lms/envs/common.py b/lms/envs/common.py index 42898fae2c..b62ed7413a 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -418,6 +418,10 @@ FEATURES = { # Whether HTML XBlocks/XModules return HTML content with the Course Blocks API student_view_data 'ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA': False, + + # Whether to send an email for failed password reset attempts or not. This is mainly useful for notifying users + # that they don't have an account associated with email addresses they believe they've registered with. + 'ENABLE_PASSWORD_RESET_FAILURE_EMAIL': False, } # Settings for the course reviews tool template and identification key, set either to None to disable course reviews diff --git a/lms/templates/registration/password_reset_email.html b/lms/templates/registration/password_reset_email.html index c41419d4a5..611863b5b1 100644 --- a/lms/templates/registration/password_reset_email.html +++ b/lms/templates/registration/password_reset_email.html @@ -1,6 +1,11 @@ {% load i18n %}{% autoescape off %} {% blocktrans %}You're receiving this e-mail because you requested a password reset for your user account at {{ platform_name }}.{% endblocktrans %} +{% if failed %} +{% blocktrans %}However, there is currently no user account associated with your email address: {{ email_address }}.{% endblocktrans %} + +{% trans "If you did not request this change, you can ignore this email." %} +{% else %} {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} {{ protocol }}://{{ site_name }}{% url 'password_reset_confirm' uidb36=uid token=token %} @@ -9,6 +14,7 @@ {% trans "If you didn't request this change, you can disregard this email - we have not yet reset your password." %} {% trans "Thanks for using our site!" %} +{% endif %} {% blocktrans %}The {{ platform_name }} Team{% endblocktrans %}