From ba6477abefae94743e40573548ff4797029dbd25 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Fri, 18 Oct 2013 15:12:07 -0400 Subject: [PATCH] Handle all exceptions returned by django-ses. --- lms/djangoapps/bulk_email/tasks.py | 34 +++++++++++++++---- lms/djangoapps/bulk_email/tests/test_tasks.py | 21 ++++++++++-- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/bulk_email/tasks.py b/lms/djangoapps/bulk_email/tasks.py index 2dd168aeb5..6f1bfd005b 100644 --- a/lms/djangoapps/bulk_email/tasks.py +++ b/lms/djangoapps/bulk_email/tasks.py @@ -12,11 +12,15 @@ from time import sleep from dogapi import dog_stats_api from smtplib import SMTPServerDisconnected, SMTPDataError, SMTPConnectError, SMTPException from boto.ses.exceptions import ( + SESAddressNotVerifiedError, + SESIdentityNotVerifiedError, + SESDomainNotConfirmedError, + SESAddressBlacklistedError, SESDailyQuotaExceededError, SESMaxSendingRateExceededError, - SESAddressBlacklistedError, - SESIllegalAddressError, + SESDomainEndsWithDotError, SESLocalAddressCharacterError, + SESIllegalAddressError, ) from boto.exception import AWSConnectionError @@ -50,11 +54,20 @@ log = get_task_logger(__name__) # Errors that an individual email is failing to be sent, and should just # be treated as a fail. -SINGLE_EMAIL_FAILURE_ERRORS = (SESAddressBlacklistedError, SESIllegalAddressError, SESLocalAddressCharacterError) +SINGLE_EMAIL_FAILURE_ERRORS = ( + SESAddressBlacklistedError, # Recipient's email address has been temporarily blacklisted. + SESDomainEndsWithDotError, # Recipient's email address' domain ends with a period/dot. + SESIllegalAddressError, # Raised when an illegal address is encountered. + SESLocalAddressCharacterError, # An address contained a control or whitespace character. +) # Exceptions that, if caught, should cause the task to be re-tried. # These errors will be caught a limited number of times before the task fails. -LIMITED_RETRY_ERRORS = (SMTPConnectError, SMTPServerDisconnected, AWSConnectionError) +LIMITED_RETRY_ERRORS = ( + SMTPConnectError, + SMTPServerDisconnected, + AWSConnectionError, +) # Errors that indicate that a mailing task should be retried without limit. # An example is if email is being sent too quickly, but may succeed if sent @@ -63,12 +76,21 @@ LIMITED_RETRY_ERRORS = (SMTPConnectError, SMTPServerDisconnected, AWSConnectionE # Note that the SMTPDataErrors here are only those within the 4xx range. # Those not in this range (i.e. in the 5xx range) are treated as hard failures # and thus like SINGLE_EMAIL_FAILURE_ERRORS. -INFINITE_RETRY_ERRORS = (SESMaxSendingRateExceededError, SMTPDataError) +INFINITE_RETRY_ERRORS = ( + SESMaxSendingRateExceededError, # Your account's requests/second limit has been exceeded. + SMTPDataError, +) # Errors that are known to indicate an inability to send any more emails, # and should therefore not be retried. For example, exceeding a quota for emails. # Also, any SMTP errors that are not explicitly enumerated above. -BULK_EMAIL_FAILURE_ERRORS = (SESDailyQuotaExceededError, SMTPException) +BULK_EMAIL_FAILURE_ERRORS = ( + SESAddressNotVerifiedError, # Raised when a "Reply-To" address has not been validated in SES yet. + SESIdentityNotVerifiedError, # Raised when an identity has not been verified in SES yet. + SESDomainNotConfirmedError, # Raised when domain ownership is not confirmed for DKIM. + SESDailyQuotaExceededError, # 24-hour allotment of outbound email has been exceeded. + SMTPException, +) def _get_recipient_queryset(user_id, to_option, course_id, course_location): diff --git a/lms/djangoapps/bulk_email/tests/test_tasks.py b/lms/djangoapps/bulk_email/tests/test_tasks.py index e49a04147a..192903c4a1 100644 --- a/lms/djangoapps/bulk_email/tests/test_tasks.py +++ b/lms/djangoapps/bulk_email/tests/test_tasks.py @@ -11,11 +11,15 @@ from itertools import cycle, chain, repeat from mock import patch, Mock from smtplib import SMTPServerDisconnected, SMTPDataError, SMTPConnectError, SMTPAuthenticationError from boto.ses.exceptions import ( + SESAddressNotVerifiedError, + SESIdentityNotVerifiedError, + SESDomainNotConfirmedError, + SESAddressBlacklistedError, SESDailyQuotaExceededError, SESMaxSendingRateExceededError, - SESAddressBlacklistedError, - SESIllegalAddressError, + SESDomainEndsWithDotError, SESLocalAddressCharacterError, + SESIllegalAddressError, ) from boto.exception import AWSConnectionError @@ -271,6 +275,10 @@ class TestBulkEmailInstructorTask(InstructorTaskCourseTestCase): # Test that celery handles permanent SMTPDataErrors by failing and not retrying. self._test_email_address_failures(SESLocalAddressCharacterError(554, "Email address contains a bad character")) + def test_ses_domain_ends_with_dot(self): + # Test that celery handles permanent SMTPDataErrors by failing and not retrying. + self._test_email_address_failures(SESDomainEndsWithDotError(554, "Email address ends with a dot")) + def _test_retry_after_limited_retry_error(self, exception): """Test that celery handles connection failures by retrying.""" # If we want the batch to succeed, we need to send fewer emails @@ -396,3 +404,12 @@ class TestBulkEmailInstructorTask(InstructorTaskCourseTestCase): def test_failure_on_ses_quota_exceeded(self): self._test_immediate_failure(SESDailyQuotaExceededError(403, "You're done for the day!")) + + def test_failure_on_ses_address_not_verified(self): + self._test_immediate_failure(SESAddressNotVerifiedError(403, "Who *are* you?")) + + def test_failure_on_ses_identity_not_verified(self): + self._test_immediate_failure(SESIdentityNotVerifiedError(403, "May I please see an ID!")) + + def test_failure_on_ses_domain_not_confirmed(self): + self._test_immediate_failure(SESDomainNotConfirmedError(403, "You're out of bounds!"))