diff --git a/cms/envs/test.py b/cms/envs/test.py index 0a7b75f772..3b08d4f61d 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -338,3 +338,4 @@ OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application' # Used with Email sending RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5 +RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5 diff --git a/common/djangoapps/student/tasks.py b/common/djangoapps/student/tasks.py new file mode 100644 index 0000000000..e745c4ab20 --- /dev/null +++ b/common/djangoapps/student/tasks.py @@ -0,0 +1,55 @@ +""" +This file contains celery tasks for sending email +""" +from django.conf import settings +from django.core import mail + +from celery.task import task # pylint: disable=no-name-in-module, import-error +from celery.utils.log import get_task_logger # pylint: disable=no-name-in-module, import-error +from celery.exceptions import MaxRetriesExceededError +from boto.exception import NoAuthHandlerFound +log = get_task_logger(__name__) + + +@task(bind=True) +def send_activation_email(self, user, subject, message, from_address): + """ + Sending an activation email to the users. + """ + max_retries = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS + retries = self.request.retries + 1 + dest_addr = user.email + try: + if settings.FEATURES.get('REROUTE_ACTIVATION_EMAIL'): + dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL'] + message = ("Activation for %s (%s): %s\n" % (user, user.email, user.profile.name) + + '-' * 80 + '\n\n' + message) + mail.send_mail(subject, message, from_address, [dest_addr], fail_silently=False) + else: + user.email_user(subject, message, from_address) + # Log that the Activation Email has been sent to user without an exception + log.info("Activataion Email has been sent to User {user_email}".format( + user_email=dest_addr + )) + except NoAuthHandlerFound: # pylint: disable=broad-except + log.info('Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'. format( + dest_addr=dest_addr, + attempt=retries, + max_attempts=max_retries + 1 + )) + try: + self.retry(countdown=settings.RETRY_ACTIVATION_EMAIL_TIMEOUT, max_retries=max_retries) + except MaxRetriesExceededError: + log.error( + 'Unable to send activation email to user from "%s" to "%s"', + from_address, + dest_addr, + exc_info=True + ) + except: # pylint: disable=bare-except + log.exception( + 'Unable to send activation email to user from "%s" to "%s"', + from_address, + dest_addr, + exc_info=True + ) diff --git a/common/djangoapps/student/tests/test_tasks.py b/common/djangoapps/student/tests/test_tasks.py new file mode 100644 index 0000000000..1c568ae416 --- /dev/null +++ b/common/djangoapps/student/tests/test_tasks.py @@ -0,0 +1,47 @@ +""" +Tests for the Sending activation email celery tasks +""" + +import mock + +from django.test import TestCase +from django.conf import settings +from student.tasks import send_activation_email +from boto.exception import NoAuthHandlerFound + + +class SendActivationEmailTestCase(TestCase): + """ + Test for send activation email to user + """ + @mock.patch('time.sleep', mock.Mock(return_value=None)) + @mock.patch('student.tasks.log') + @mock.patch('django.contrib.auth.models.User') + def test_send_email(self, mock_user, mock_log): + """ + Tests retries when the activation email doesn't send + """ + from_address = 'task_testing@edX.com' + mock_user.email_user.side_effect = NoAuthHandlerFound + email_max_attempts = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS + 1 + + send_activation_email.delay(mock_user, 'Task_test', 'Task_test_message', from_address) + + # Asserts sending email retry logging. + for attempt in xrange(1, email_max_attempts): + mock_log.info.assert_any_call( + 'Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format( + dest_addr=mock_user.email, + attempt=attempt, + max_attempts=email_max_attempts + )) + self.assertEquals(mock_log.info.call_count, 6) + + # Asserts that the error was logged on crossing max retry attempts. + mock_log.error.assert_called_with( + 'Unable to send activation email to user from "%s" to "%s"', + from_address, + mock_user.email, + exc_info=True + ) + self.assertEquals(mock_log.error.call_count, 1) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b63d293999..29519860f3 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -56,6 +56,7 @@ from student.models import ( DashboardConfiguration, LinkedInAddToProfileConfiguration, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED, LogoutViewConfiguration) from student.forms import AccountCreationForm, PasswordResetFormNoActive, get_registration_extension_form +from student.tasks import send_activation_email from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error from bulk_email.models import Optout, BulkEmailFlag # pylint: disable=import-error @@ -1775,12 +1776,10 @@ def create_account_with_params(request, params): ) ) if send_email: - dest_addr = user.email context = { 'name': profile.name, 'key': registration.activation_key, } - # composes activation email subject = render_to_string('emails/activation_email_subject.txt', context) # Email subject *must not* contain newlines @@ -1791,32 +1790,7 @@ def create_account_with_params(request, params): 'email_from_address', settings.DEFAULT_FROM_EMAIL ) - retries = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS - while retries: - try: - if settings.FEATURES.get('REROUTE_ACTIVATION_EMAIL'): - dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL'] - message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) + - '-' * 80 + '\n\n' + message) - mail.send_mail(subject, message, from_address, [dest_addr], fail_silently=False) - else: - user.email_user(subject, message, from_address) - # Log that the Activation Email has been sent to user without an exception - log.info("Activataion Email has been sent to User {user_email}".format( - user_email=dest_addr - )) - break - except Exception: # pylint: disable=broad-except - retries -= 1 - if retries <= 0: - break - log.error( - u'Unable to send activation email to user from "%s" to "%s"', - from_address, - dest_addr, - exc_info=True - ) - + send_activation_email.delay(user, subject, message, from_address) else: registration.activate() _enroll_user_in_pending_courses(user) # Enroll student in any pending courses diff --git a/lms/envs/common.py b/lms/envs/common.py index 5bce4db235..47adc782f9 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -372,6 +372,7 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds # Used with Email sending RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5 +RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5 ############################# SET PATH INFORMATION #############################