Beispiel #1
0
 def clean_name(self):
     """Validate the name field. Enforce uniqueness constraint on 'name' field"""
     name = self.cleaned_data.get("name")
     # if we are creating a new CourseEmailTemplate, then we need to
     # enforce the uniquess constraint as part of the Form validation
     if not self.instance.pk:
         try:
             CourseEmailTemplate.get_template(name)
             # already exists, this is no good
             raise ValidationError('Name of "{}" already exists, this must be unique.'.format(name))
         except CourseEmailTemplate.DoesNotExist:
             # this is actually the successful validation
             pass
     return name
 def test_get_branded_template(self):
     # Get a branded (non default) template and make sure we get what we expect
     template = CourseEmailTemplate.get_template(name="branded.template")
     self.assertIsNotNone(template.html_template)
     self.assertIsNotNone(template.plain_template)
     self.assertIn(u"THIS IS A BRANDED HTML TEMPLATE", template.html_template)
     self.assertIn(u"THIS IS A BRANDED TEXT TEMPLATE", template.plain_template)
    def send_email_to_student(self, receivers, subject, text):
        # Instead of sending the email through the rest of the edX bulk mail system,
        # we're going to use the edX email templater, and then toss the email directly through
        # the Django mailer.

        # We're assuming receivers is a list of User IDs.

        emails = []

        email_template = CourseEmailTemplate.get_template()
        context = get_email_context(CourseData.get_course(self.course_id))
        from_address = get_source_address(self.course_id, self.acquire_course_name())

        for student_id in list(set(receivers)):
            context['email'] = self.acquire_student_email(student_id)
            context['name'] = self.acquire_student_name(student_id)

            plaintext_message = email_template.render_plaintext(text, context)
            html_message = email_template.render_htmltext(text, context)

            email_message = mail.EmailMultiAlternatives(subject, plaintext_message, from_address, [context['email']])
            email_message.attach_alternative(html_message, 'text/html')

            emails.append(email_message)

        connection = mail.get_connection()
        connection.send_messages(emails)

        return
 def test_render_plaintext_without_context(self):
     template = CourseEmailTemplate.get_template()
     base_context = self._get_sample_plain_context()
     for keyname in base_context:
         context = dict(base_context)
         del context[keyname]
         with self.assertRaises(KeyError):
             template.render_plaintext("My new plain text.", context)
Beispiel #5
0
 def test_render_plaintext_without_context(self):
     template = CourseEmailTemplate.get_template()
     base_context = self._get_sample_plain_context()
     for keyname in base_context:
         context = dict(base_context)
         del context[keyname]
         with self.assertRaises(KeyError):
             template.render_plaintext("My new plain text.", context)
Beispiel #6
0
    def clean_name(self):
        """Validate the name field. Enforce uniqueness constraint on 'name' field"""

        # Note that we get back a blank string in the Form for an empty 'name' field
        # we want those to be set to None in Python and NULL in the database
        name = self.cleaned_data.get("name").strip() or None

        # if we are creating a new CourseEmailTemplate, then we need to
        # enforce the uniquess constraint as part of the Form validation
        if not self.instance.pk:
            try:
                CourseEmailTemplate.get_template(name)
                # already exists, this is no good
                raise ValidationError('Name of "{}" already exists, this must be unique.'.format(name))
            except CourseEmailTemplate.DoesNotExist:
                # this is actually the successful validation
                pass
        return name
Beispiel #7
0
    def clean_name(self):
        """Validate the name field. Enforce uniqueness constraint on 'name' field"""

        # Note that we get back a blank string in the Form for an empty 'name' field
        # we want those to be set to None in Python and NULL in the database
        name = self.cleaned_data.get("name").strip() or None

        # if we are creating a new CourseEmailTemplate, then we need to
        # enforce the uniquess constraint as part of the Form validation
        if not self.instance.pk:
            try:
                CourseEmailTemplate.get_template(name)
                # already exists, this is no good
                raise ValidationError('Name of "{}" already exists, this must be unique.'.format(name))
            except CourseEmailTemplate.DoesNotExist:
                # this is actually the successful validation
                pass
        return name
Beispiel #8
0
 def test_render_plain_no_escaping(self):
     template = CourseEmailTemplate.get_template()
     context = self._add_xss_fields(self._get_sample_plain_context())
     message = template.render_plaintext(
         "Dear %%USER_FULLNAME%%, thanks for enrolling in %%COURSE_DISPLAY_NAME%%.", context
     )
     self.assertNotIn("<script>", message)
     self.assertIn(context['course_title'], message)
     self.assertIn(context['name'], message)
Beispiel #9
0
 def test_get_branded_template(self):
     # Get a branded (non default) template and make sure we get what we expect
     template = CourseEmailTemplate.get_template(name="branded.template")
     self.assertIsNotNone(template.html_template)
     self.assertIsNotNone(template.plain_template)
     self.assertIn(u"THIS IS A BRANDED HTML TEMPLATE",
                   template.html_template)
     self.assertIn(u"THIS IS A BRANDED TEXT TEMPLATE",
                   template.plain_template)
Beispiel #10
0
 def test_render_html_xss(self):
     template = CourseEmailTemplate.get_template()
     context = self._add_xss_fields(self._get_sample_html_context())
     message = template.render_htmltext(
         "Dear %%USER_FULLNAME%%, thanks for enrolling in %%COURSE_DISPLAY_NAME%%.", context
     )
     self.assertNotIn("<script>", message)
     self.assertIn("&lt;script&gt;alert(&#39;Course Title!&#39;);&lt;/alert&gt;", message)
     self.assertIn("&lt;script&gt;alert(&#39;Profile Name!&#39;);&lt;/alert&gt;", message)
 def test_render_plain_no_escaping(self):
     template = CourseEmailTemplate.get_template()
     context = self._add_xss_fields(self._get_sample_plain_context())
     message = template.render_plaintext(
         "Dear %%USER_FULLNAME%%, thanks for enrolling in %%COURSE_DISPLAY_NAME%%.", context
     )
     self.assertNotIn("&lt;script&gt;", message)
     self.assertIn(context['course_title'], message)
     self.assertIn(context['name'], message)
 def test_render_html_xss(self):
     template = CourseEmailTemplate.get_template()
     context = self._add_xss_fields(self._get_sample_html_context())
     message = template.render_htmltext(
         "Dear %%USER_FULLNAME%%, thanks for enrolling in %%COURSE_DISPLAY_NAME%%.", context
     )
     self.assertNotIn("<script>", message)
     self.assertIn("&lt;script&gt;alert(&#39;Course Title!&#39;);&lt;/alert&gt;", message)
     self.assertIn("&lt;script&gt;alert(&#39;Profile Name!&#39;);&lt;/alert&gt;", message)
Beispiel #13
0
 def test_render_html(self):
     template = CourseEmailTemplate.get_template()
     context = self._get_sample_html_context()
     template.render_htmltext("My new html text.", context)
Beispiel #14
0
 def test_get_template(self):
     # Get the default template, which has name=None
     template = CourseEmailTemplate.get_template()
     self.assertIsNotNone(template.html_template)
     self.assertIsNotNone(template.plain_template)
Beispiel #15
0
 def test_get_missing_template(self):
     with self.assertRaises(CourseEmailTemplate.DoesNotExist):
         CourseEmailTemplate.get_template()
Beispiel #16
0
def _send_course_email(entry_id, email_id, to_list, global_email_context, subtask_status):
    """
    Performs the email sending task.

    Sends an email to a list of recipients.

    Inputs are:
      * `entry_id`: id of the InstructorTask object to which progress should be recorded.
      * `email_id`: id of the CourseEmail model that is to be emailed.
      * `to_list`: list of recipients.  Each is represented as a dict with the following keys:
        - 'profile__name': full name of User.
        - 'email': email address of User.
        - 'pk': primary key of User model.
      * `global_email_context`: dict containing values that are unique for this email but the same
        for all recipients of this email.  This dict is to be used to fill in slots in email
        template.  It does not include 'name' and 'email', which will be provided by the to_list.
      * `subtask_status` : dict containing values representing current status.  Keys are:

        'task_id' : id of subtask.  This is used to pass task information across retries.
        'attempted' : number of attempts -- should equal succeeded plus failed
        'succeeded' : number that succeeded in processing
        'skipped' : number that were not processed.
        'failed' : number that failed during processing
        'retried_nomax' : number of times the subtask has been retried for conditions that
            should not have a maximum count applied
        'retried_withmax' : number of times the subtask has been retried for conditions that
            should have a maximum count applied
        'state' : celery state of the subtask (e.g. QUEUING, PROGRESS, RETRY, FAILURE, SUCCESS)

    Sends to all addresses contained in to_list that are not also in the Optout table.
    Emails are sent multi-part, in both plain text and html.

    Returns a tuple of two values:
      * First value is a dict which represents current progress at the end of this call.  Keys are
        the same as for the input subtask_status.

      * Second value is an exception returned by the innards of the method, indicating a fatal error.
        In this case, the number of recipients that were not sent have already been added to the
        'failed' count above.
    """
    # Get information from current task's request:
    task_id = subtask_status['task_id']

    # collect stats on progress:
    num_optout = 0
    num_sent = 0
    num_error = 0

    try:
        course_email = CourseEmail.objects.get(id=email_id)
    except CourseEmail.DoesNotExist as exc:
        log.exception("Task %s: could not find email id:%s to send.", task_id, email_id)
        raise

    # Exclude optouts (if not a retry):
    # Note that we don't have to do the optout logic at all if this is a retry,
    # because we have presumably already performed the optout logic on the first
    # attempt.  Anyone on the to_list on a retry has already passed the filter
    # that existed at that time, and we don't need to keep checking for changes
    # in the Optout list.
    if (subtask_status['retried_nomax'] + subtask_status['retried_withmax']) == 0:
        to_list, num_optout = _filter_optouts_from_recipients(to_list, course_email.course_id)

    course_title = global_email_context['course_title']
    subject = "[" + course_title + "] " + course_email.subject
    from_addr = _get_source_address(course_email.course_id, course_title)

    course_email_template = CourseEmailTemplate.get_template()
    try:
        connection = get_connection()
        connection.open()

        # Define context values to use in all course emails:
        email_context = {'name': '', 'email': ''}
        email_context.update(global_email_context)

        while to_list:
            # Update context with user-specific values from the user at the end of the list.
            # At the end of processing this user, they will be popped off of the to_list.
            # That way, the to_list will always contain the recipients remaining to be emailed.
            # This is convenient for retries, which will need to send to those who haven't
            # yet been emailed, but not send to those who have already been sent to.
            current_recipient = to_list[-1]
            email = current_recipient['email']
            email_context['email'] = email
            email_context['name'] = current_recipient['profile__name']

            # Construct message content using templates and context:
            plaintext_msg = course_email_template.render_plaintext(course_email.text_message, email_context)
            html_msg = course_email_template.render_htmltext(course_email.html_message, email_context)

            # Create email:
            email_msg = EmailMultiAlternatives(
                subject,
                plaintext_msg,
                from_addr,
                [email],
                connection=connection
            )
            email_msg.attach_alternative(html_msg, 'text/html')

            # Throttle if we have gotten the rate limiter.  This is not very high-tech,
            # but if a task has been retried for rate-limiting reasons, then we sleep
            # for a period of time between all emails within this task.  Choice of
            # the value depends on the number of workers that might be sending email in
            # parallel, and what the SES throttle rate is.
            if subtask_status['retried_nomax'] > 0:
                sleep(settings.BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS)

            try:
                log.debug('Email with id %s to be sent to %s', email_id, email)

                with dog_stats_api.timer('course_email.single_send.time.overall', tags=[_statsd_tag(course_title)]):
                    connection.send_messages([email_msg])

            except SMTPDataError as exc:
                # According to SMTP spec, we'll retry error codes in the 4xx range.  5xx range indicates hard failure.
                if exc.smtp_code >= 400 and exc.smtp_code < 500:
                    # This will cause the outer handler to catch the exception and retry the entire task.
                    raise exc
                else:
                    # This will fall through and not retry the message.
                    log.warning('Task %s: email with id %s not delivered to %s due to error %s', task_id, email_id, email, exc.smtp_error)
                    dog_stats_api.increment('course_email.error', tags=[_statsd_tag(course_title)])
                    num_error += 1

            except SINGLE_EMAIL_FAILURE_ERRORS as exc:
                # This will fall through and not retry the message.
                log.warning('Task %s: email with id %s not delivered to %s due to error %s', task_id, email_id, email, exc)
                dog_stats_api.increment('course_email.error', tags=[_statsd_tag(course_title)])
                num_error += 1

            else:
                dog_stats_api.increment('course_email.sent', tags=[_statsd_tag(course_title)])
                if settings.BULK_EMAIL_LOG_SENT_EMAILS:
                    log.info('Email with id %s sent to %s', email_id, email)
                else:
                    log.debug('Email with id %s sent to %s', email_id, email)
                num_sent += 1

            # Pop the user that was emailed off the end of the list only once they have
            # successfully been processed.  (That way, if there were a failure that
            # needed to be retried, the user is still on the list.)
            to_list.pop()

    except INFINITE_RETRY_ERRORS as exc:
        dog_stats_api.increment('course_email.infinite_retry', tags=[_statsd_tag(course_title)])
        # Increment the "retried_nomax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_progress = increment_subtask_status(
            subtask_status,
            succeeded=num_sent,
            failed=num_error,
            skipped=num_optout,
            retried_nomax=1,
            state=RETRY
        )
        return _submit_for_retry(
            entry_id, email_id, to_list, global_email_context, exc, subtask_progress, skip_retry_max=True
        )

    except LIMITED_RETRY_ERRORS as exc:
        # Errors caught here cause the email to be retried.  The entire task is actually retried
        # without popping the current recipient off of the existing list.
        # Errors caught are those that indicate a temporary condition that might succeed on retry.
        dog_stats_api.increment('course_email.limited_retry', tags=[_statsd_tag(course_title)])
        # Increment the "retried_withmax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_progress = increment_subtask_status(
            subtask_status,
            succeeded=num_sent,
            failed=num_error,
            skipped=num_optout,
            retried_withmax=1,
            state=RETRY
        )
        return _submit_for_retry(
            entry_id, email_id, to_list, global_email_context, exc, subtask_progress, skip_retry_max=False
        )

    except BULK_EMAIL_FAILURE_ERRORS as exc:
        dog_stats_api.increment('course_email.error', tags=[_statsd_tag(course_title)])
        num_pending = len(to_list)
        log.exception('Task %s: email with id %d caused send_course_email task to fail with "fatal" exception.  %d emails unsent.',
                      task_id, email_id, num_pending)
        # Update counters with progress to date, counting unsent emails as failures,
        # and set the state to FAILURE:
        subtask_progress = increment_subtask_status(
            subtask_status,
            succeeded=num_sent,
            failed=(num_error + num_pending),
            skipped=num_optout,
            state=FAILURE
        )
        return subtask_progress, exc

    except Exception as exc:
        # Errors caught here cause the email to be retried.  The entire task is actually retried
        # without popping the current recipient off of the existing list.
        # These are unexpected errors.  Since they might be due to a temporary condition that might
        # succeed on retry, we give them a retry.
        dog_stats_api.increment('course_email.limited_retry', tags=[_statsd_tag(course_title)])
        log.exception('Task %s: email with id %d caused send_course_email task to fail with unexpected exception.  Generating retry.',
                      task_id, email_id)
        # Increment the "retried_withmax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_progress = increment_subtask_status(
            subtask_status,
            succeeded=num_sent,
            failed=num_error,
            skipped=num_optout,
            retried_withmax=1,
            state=RETRY
        )
        return _submit_for_retry(
            entry_id, email_id, to_list, global_email_context, exc, subtask_progress, skip_retry_max=False
        )

    else:
        # All went well.  Update counters with progress to date,
        # and set the state to SUCCESS:
        subtask_progress = increment_subtask_status(
            subtask_status,
            succeeded=num_sent,
            failed=num_error,
            skipped=num_optout,
            state=SUCCESS
        )
        # Successful completion is marked by an exception value of None.
        return subtask_progress, None
    finally:
        # Clean up at the end.
        connection.close()
Beispiel #17
0
 def test_get_template(self):
     template = CourseEmailTemplate.get_template()
     self.assertIsNotNone(template.html_template)
     self.assertIsNotNone(template.plain_template)
 def test_render_html(self):
     template = CourseEmailTemplate.get_template()
     context = self._get_sample_html_context()
     template.render_htmltext("My new html text.", context)
Beispiel #19
0
def _send_course_email(entry_id, email_id, to_list, global_email_context,
                       subtask_status):
    """
    Performs the email sending task.

    Sends an email to a list of recipients.

    Inputs are:
      * `entry_id`: id of the InstructorTask object to which progress should be recorded.
      * `email_id`: id of the CourseEmail model that is to be emailed.
      * `to_list`: list of recipients.  Each is represented as a dict with the following keys:
        - 'profile__name': full name of User.
        - 'email': email address of User.
        - 'pk': primary key of User model.
      * `global_email_context`: dict containing values that are unique for this email but the same
        for all recipients of this email.  This dict is to be used to fill in slots in email
        template.  It does not include 'name' and 'email', which will be provided by the to_list.
      * `subtask_status` : object of class SubtaskStatus representing current status.

    Sends to all addresses contained in to_list that are not also in the Optout table.
    Emails are sent multi-part, in both plain text and html.

    Returns a tuple of two values:
      * First value is a SubtaskStatus object which represents current progress at the end of this call.

      * Second value is an exception returned by the innards of the method, indicating a fatal error.
        In this case, the number of recipients that were not sent have already been added to the
        'failed' count above.
    """
    # Get information from current task's request:
    task_id = subtask_status.task_id

    try:
        course_email = CourseEmail.objects.get(id=email_id)
    except CourseEmail.DoesNotExist as exc:
        log.exception("Task %s: could not find email id:%s to send.", task_id,
                      email_id)
        raise

    # Exclude optouts (if not a retry):
    # Note that we don't have to do the optout logic at all if this is a retry,
    # because we have presumably already performed the optout logic on the first
    # attempt.  Anyone on the to_list on a retry has already passed the filter
    # that existed at that time, and we don't need to keep checking for changes
    # in the Optout list.
    if subtask_status.get_retry_count() == 0:
        to_list, num_optout = _filter_optouts_from_recipients(
            to_list, course_email.course_id)
        subtask_status.increment(skipped=num_optout)

    course_title = global_email_context['course_title']
    subject = "[" + course_title + "] " + course_email.subject
    from_addr = _get_source_address(course_email.course_id, course_title)

    course_email_template = CourseEmailTemplate.get_template()
    try:
        connection = get_connection()
        connection.open()

        # Define context values to use in all course emails:
        email_context = {'name': '', 'email': ''}
        email_context.update(global_email_context)

        while to_list:
            # Update context with user-specific values from the user at the end of the list.
            # At the end of processing this user, they will be popped off of the to_list.
            # That way, the to_list will always contain the recipients remaining to be emailed.
            # This is convenient for retries, which will need to send to those who haven't
            # yet been emailed, but not send to those who have already been sent to.
            current_recipient = to_list[-1]
            email = current_recipient['email']
            email_context['email'] = email
            email_context['name'] = current_recipient['profile__name']

            # Construct message content using templates and context:
            plaintext_msg = course_email_template.render_plaintext(
                course_email.text_message, email_context)
            html_msg = course_email_template.render_htmltext(
                course_email.html_message, email_context)

            # Create email:
            email_msg = EmailMultiAlternatives(subject,
                                               plaintext_msg,
                                               from_addr, [email],
                                               connection=connection)
            email_msg.attach_alternative(html_msg, 'text/html')

            # Throttle if we have gotten the rate limiter.  This is not very high-tech,
            # but if a task has been retried for rate-limiting reasons, then we sleep
            # for a period of time between all emails within this task.  Choice of
            # the value depends on the number of workers that might be sending email in
            # parallel, and what the SES throttle rate is.
            if subtask_status.retried_nomax > 0:
                sleep(settings.BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS)

            try:
                log.debug('Email with id %s to be sent to %s', email_id, email)

                with dog_stats_api.timer(
                        'course_email.single_send.time.overall',
                        tags=[_statsd_tag(course_title)]):
                    connection.send_messages([email_msg])

            except SMTPDataError as exc:
                # According to SMTP spec, we'll retry error codes in the 4xx range.  5xx range indicates hard failure.
                if exc.smtp_code >= 400 and exc.smtp_code < 500:
                    # This will cause the outer handler to catch the exception and retry the entire task.
                    raise exc
                else:
                    # This will fall through and not retry the message.
                    log.warning(
                        'Task %s: email with id %s not delivered to %s due to error %s',
                        task_id, email_id, email, exc.smtp_error)
                    dog_stats_api.increment('course_email.error',
                                            tags=[_statsd_tag(course_title)])
                    subtask_status.increment(failed=1)

            except SINGLE_EMAIL_FAILURE_ERRORS as exc:
                # This will fall through and not retry the message.
                log.warning(
                    'Task %s: email with id %s not delivered to %s due to error %s',
                    task_id, email_id, email, exc)
                dog_stats_api.increment('course_email.error',
                                        tags=[_statsd_tag(course_title)])
                subtask_status.increment(failed=1)

            else:
                dog_stats_api.increment('course_email.sent',
                                        tags=[_statsd_tag(course_title)])
                if settings.BULK_EMAIL_LOG_SENT_EMAILS:
                    log.info('Email with id %s sent to %s', email_id, email)
                else:
                    log.debug('Email with id %s sent to %s', email_id, email)
                subtask_status.increment(succeeded=1)

            # Pop the user that was emailed off the end of the list only once they have
            # successfully been processed.  (That way, if there were a failure that
            # needed to be retried, the user is still on the list.)
            to_list.pop()

    except INFINITE_RETRY_ERRORS as exc:
        dog_stats_api.increment('course_email.infinite_retry',
                                tags=[_statsd_tag(course_title)])
        # Increment the "retried_nomax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_status.increment(retried_nomax=1, state=RETRY)
        return _submit_for_retry(entry_id,
                                 email_id,
                                 to_list,
                                 global_email_context,
                                 exc,
                                 subtask_status,
                                 skip_retry_max=True)

    except LIMITED_RETRY_ERRORS as exc:
        # Errors caught here cause the email to be retried.  The entire task is actually retried
        # without popping the current recipient off of the existing list.
        # Errors caught are those that indicate a temporary condition that might succeed on retry.
        dog_stats_api.increment('course_email.limited_retry',
                                tags=[_statsd_tag(course_title)])
        # Increment the "retried_withmax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_status.increment(retried_withmax=1, state=RETRY)
        return _submit_for_retry(entry_id,
                                 email_id,
                                 to_list,
                                 global_email_context,
                                 exc,
                                 subtask_status,
                                 skip_retry_max=False)

    except BULK_EMAIL_FAILURE_ERRORS as exc:
        dog_stats_api.increment('course_email.error',
                                tags=[_statsd_tag(course_title)])
        num_pending = len(to_list)
        log.exception(
            'Task %s: email with id %d caused send_course_email task to fail with "fatal" exception.  %d emails unsent.',
            task_id, email_id, num_pending)
        # Update counters with progress to date, counting unsent emails as failures,
        # and set the state to FAILURE:
        subtask_status.increment(failed=num_pending, state=FAILURE)
        return subtask_status, exc

    except Exception as exc:
        # Errors caught here cause the email to be retried.  The entire task is actually retried
        # without popping the current recipient off of the existing list.
        # These are unexpected errors.  Since they might be due to a temporary condition that might
        # succeed on retry, we give them a retry.
        dog_stats_api.increment('course_email.limited_retry',
                                tags=[_statsd_tag(course_title)])
        log.exception(
            'Task %s: email with id %d caused send_course_email task to fail with unexpected exception.  Generating retry.',
            task_id, email_id)
        # Increment the "retried_withmax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_status.increment(retried_withmax=1, state=RETRY)
        return _submit_for_retry(entry_id,
                                 email_id,
                                 to_list,
                                 global_email_context,
                                 exc,
                                 subtask_status,
                                 skip_retry_max=False)

    else:
        # All went well.  Update counters with progress to date,
        # and set the state to SUCCESS:
        subtask_status.increment(state=SUCCESS)
        # Successful completion is marked by an exception value of None.
        return subtask_status, None
    finally:
        # Clean up at the end.
        connection.close()
Beispiel #20
0
def _send_course_email(email_id, to_list, course_title, course_url, image_url,
                       throttle):
    """
    Performs the email sending task.
    """
    try:
        msg = CourseEmail.objects.get(id=email_id)
    except CourseEmail.DoesNotExist:
        log.exception("Could not find email id:{} to send.".format(email_id))
        raise

    # exclude optouts
    optouts = (Optout.objects.filter(course_id=msg.course_id,
                                     user__in=[i['pk'] for i in to_list
                                               ]).values_list('user__email',
                                                              flat=True))

    optouts = set(optouts)
    num_optout = len(optouts)

    to_list = [
        recipient for recipient in to_list if recipient['email'] not in optouts
    ]

    subject = "[" + course_title + "] " + msg.subject

    course_title_no_quotes = re.sub(r'"', '', course_title)
    from_addr = '"{0}" Course Staff <{1}>'.format(
        course_title_no_quotes, settings.DEFAULT_BULK_FROM_EMAIL)

    course_email_template = CourseEmailTemplate.get_template()

    try:
        connection = get_connection()
        connection.open()
        num_sent = 0
        num_error = 0

        # Define context values to use in all course emails:
        email_context = {
            'name':
            '',
            'email':
            '',
            'course_title':
            course_title,
            'course_url':
            course_url,
            'course_image_url':
            image_url,
            'account_settings_url':
            'https://{}{}'.format(settings.SITE_NAME, reverse('dashboard')),
            'platform_name':
            settings.PLATFORM_NAME,
        }

        while to_list:
            # Update context with user-specific values:
            email = to_list[-1]['email']
            email_context['email'] = email
            email_context['name'] = to_list[-1]['profile__name']

            # Construct message content using templates and context:
            plaintext_msg = course_email_template.render_plaintext(
                msg.text_message, email_context)
            html_msg = course_email_template.render_htmltext(
                msg.html_message, email_context)

            # Create email:
            email_msg = EmailMultiAlternatives(subject,
                                               plaintext_msg,
                                               from_addr, [email],
                                               connection=connection)
            email_msg.attach_alternative(html_msg, 'text/html')

            # Throttle if we tried a few times and got the rate limiter
            if throttle or current_task.request.retries > 0:
                time.sleep(0.2)

            try:
                with dog_stats_api.timer(
                        'course_email.single_send.time.overall',
                        tags=[_statsd_tag(course_title)]):
                    connection.send_messages([email_msg])

                dog_stats_api.increment('course_email.sent',
                                        tags=[_statsd_tag(course_title)])

                log.info('Email with id %s sent to %s', email_id, email)
                num_sent += 1
            except SMTPDataError as exc:
                # According to SMTP spec, we'll retry error codes in the 4xx range.  5xx range indicates hard failure
                if exc.smtp_code >= 400 and exc.smtp_code < 500:
                    # This will cause the outer handler to catch the exception and retry the entire task
                    raise exc
                else:
                    # This will fall through and not retry the message, since it will be popped
                    log.warning(
                        'Email with id %s not delivered to %s due to error %s',
                        email_id, email, exc.smtp_error)

                    dog_stats_api.increment('course_email.error',
                                            tags=[_statsd_tag(course_title)])

                    num_error += 1

            to_list.pop()

        connection.close()
        return course_email_result(num_sent, num_error, num_optout)

    except (SMTPDataError, SMTPConnectError, SMTPServerDisconnected) as exc:
        # Error caught here cause the email to be retried.  The entire task is actually retried without popping the list
        # Reasoning is that all of these errors may be temporary condition.
        log.warning(
            'Email with id %d not delivered due to temporary error %s, retrying send to %d recipients',
            email_id, exc, len(to_list))
        raise course_email.retry(arg=[
            email_id, to_list, course_title, course_url, image_url,
            current_task.request.retries > 0
        ],
                                 exc=exc,
                                 countdown=(2**current_task.request.retries) *
                                 15)
    except:
        log.exception(
            'Email with id %d caused course_email task to fail with uncaught exception. To list: %s',
            email_id, [i['email'] for i in to_list])
        # Close the connection before we exit
        connection.close()
        raise
 def test_get_template(self):
     # Get the default template, which has name=None
     template = CourseEmailTemplate.get_template()
     self.assertIsNotNone(template.html_template)
     self.assertIsNotNone(template.plain_template)
 def test_get_missing_template(self):
     with self.assertRaises(CourseEmailTemplate.DoesNotExist):
         CourseEmailTemplate.get_template()
Beispiel #23
0
 def test_render_plain(self):
     template = CourseEmailTemplate.get_template()
     context = self._get_sample_plain_context()
     template.render_plaintext("My new plain text.", context)
Beispiel #24
0
 def test_get_template(self):
     template = CourseEmailTemplate.get_template()
     self.assertIsNotNone(template.html_template)
     self.assertIsNotNone(template.plain_template)
 def test_render_plain(self):
     template = CourseEmailTemplate.get_template()
     context = self._get_sample_plain_context()
     template.render_plaintext("My new plain text.", context)
Beispiel #26
0
def _send_course_email(email_id, to_list, course_title, course_url, image_url, throttle):
    """
    Performs the email sending task.
    """
    try:
        msg = CourseEmail.objects.get(id=email_id)
    except CourseEmail.DoesNotExist:
        log.exception("Could not find email id:{} to send.".format(email_id))
        raise

    # exclude optouts
    optouts = (Optout.objects.filter(course_id=msg.course_id,
                                     user__in=[i['pk'] for i in to_list])
                             .values_list('user__email', flat=True))

    optouts = set(optouts)
    num_optout = len(optouts)

    to_list = [recipient for recipient in to_list if recipient['email'] not in optouts]

    subject = "[" + course_title + "] " + msg.subject

    course_title_no_quotes = re.sub(r'"', '', course_title)
    from_addr = '"{0}" Course Staff <{1}>'.format(course_title_no_quotes, settings.DEFAULT_BULK_FROM_EMAIL)

    course_email_template = CourseEmailTemplate.get_template()

    try:
        connection = get_connection()
        connection.open()
        num_sent = 0
        num_error = 0

        # Define context values to use in all course emails:
        email_context = {
            'name': '',
            'email': '',
            'course_title': course_title,
            'course_url': course_url,
            'course_image_url': image_url,
            'account_settings_url': 'https://{}{}'.format(settings.SITE_NAME, reverse('dashboard')),
            'platform_name': settings.PLATFORM_NAME,
        }

        while to_list:
            # Update context with user-specific values:
            email = to_list[-1]['email']
            email_context['email'] = email
            email_context['name'] = to_list[-1]['profile__name']

            # Construct message content using templates and context:
            plaintext_msg = course_email_template.render_plaintext(msg.text_message, email_context)
            html_msg = course_email_template.render_htmltext(msg.html_message, email_context)

            # Create email:
            email_msg = EmailMultiAlternatives(
                subject,
                plaintext_msg,
                from_addr,
                [email],
                connection=connection
            )
            email_msg.attach_alternative(html_msg, 'text/html')

            # Throttle if we tried a few times and got the rate limiter
            if throttle or current_task.request.retries > 0:
                time.sleep(0.2)

            try:
                with dog_stats_api.timer('course_email.single_send.time.overall', tags=[_statsd_tag(course_title)]):
                    connection.send_messages([email_msg])

                dog_stats_api.increment('course_email.sent', tags=[_statsd_tag(course_title)])

                log.info('Email with id %s sent to %s', email_id, email)
                num_sent += 1
            except SMTPDataError as exc:
                # According to SMTP spec, we'll retry error codes in the 4xx range.  5xx range indicates hard failure
                if exc.smtp_code >= 400 and exc.smtp_code < 500:
                    # This will cause the outer handler to catch the exception and retry the entire task
                    raise exc
                else:
                    # This will fall through and not retry the message, since it will be popped
                    log.warning('Email with id %s not delivered to %s due to error %s', email_id, email, exc.smtp_error)

                    dog_stats_api.increment('course_email.error', tags=[_statsd_tag(course_title)])

                    num_error += 1

            to_list.pop()

        connection.close()
        return course_email_result(num_sent, num_error, num_optout)

    except (SMTPDataError, SMTPConnectError, SMTPServerDisconnected) as exc:
        # Error caught here cause the email to be retried.  The entire task is actually retried without popping the list
        # Reasoning is that all of these errors may be temporary condition.
        log.warning('Email with id %d not delivered due to temporary error %s, retrying send to %d recipients',
                    email_id, exc, len(to_list))
        raise course_email.retry(
            arg=[
                email_id,
                to_list,
                course_title,
                course_url,
                image_url,
                current_task.request.retries > 0
            ],
            exc=exc,
            countdown=(2 ** current_task.request.retries) * 15
        )
    except:
        log.exception('Email with id %d caused course_email task to fail with uncaught exception. To list: %s',
                      email_id,
                      [i['email'] for i in to_list])
        # Close the connection before we exit
        connection.close()
        raise