Skip to content

Commit 7d993ee

Browse files
committed
Fix fail_silently when session/client creation fails
Make sure backends actually fail silently when asked (rather than raising inaccurate errors suggesting coding problems). Fixes #308
1 parent 1ba26e1 commit 7d993ee

7 files changed

+75
-4
lines changed

Diff for: CHANGELOG.rst

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ vNext
3333
Fixes
3434
~~~~~
3535

36+
* Fix misleading error messages when sending with ``fail_silently=True``
37+
and session creation fails (e.g., with Amazon SES backend and missing
38+
credentials). (Thanks to `@technolingo`_.)
39+
3640
* **Postmark:** Fix spurious AnymailInvalidAddress in ``message.cc`` when
3741
inbound message has no Cc recipients. (Thanks to `@Ecno92`_.)
3842

@@ -1455,6 +1459,7 @@ Features
14551459
.. _@slinkymanbyday: https://github.com/slinkymanbyday
14561460
.. _@swrobel: https://github.com/swrobel
14571461
.. _@tcourtqtm: https://github.com/tcourtqtm
1462+
.. _@technolingo: https://github.com/technolingo
14581463
.. _@Thorbenl: https://github.com/Thorbenl
14591464
.. _@tiltec: https://github.com/tiltec
14601465
.. _@tim-schilling: https://github.com/tim-schilling

Diff for: anymail/backends/amazon_ses.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def open(self):
5959
self.client = boto3.session.Session(**self.session_params).client(
6060
"ses", **self.client_params
6161
)
62-
except BOTO_BASE_ERRORS:
62+
except Exception:
6363
if not self.fail_silently:
6464
raise
6565
else:
@@ -71,6 +71,22 @@ def close(self):
7171
# self.client.close() # boto3 doesn't support (or require) client shutdown
7272
self.client = None
7373

74+
def _send(self, message):
75+
if self.client:
76+
return super()._send(message)
77+
elif self.fail_silently:
78+
# (Probably missing boto3 credentials in open().)
79+
return False
80+
else:
81+
class_name = self.__class__.__name__
82+
raise RuntimeError(
83+
"boto3 Session has not been opened in {class_name}._send. "
84+
"(This is either an implementation error in {class_name}, "
85+
"or you are incorrectly calling _send directly.)".format(
86+
class_name=class_name
87+
)
88+
)
89+
7490
def build_message_payload(self, message, defaults):
7591
# The SES SendRawEmail and SendBulkTemplatedEmail calls have
7692
# very different signatures, so use a custom payload for each

Diff for: anymail/backends/amazon_sesv2.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def open(self):
6363
self.client = boto3.session.Session(**self.session_params).client(
6464
"sesv2", **self.client_params
6565
)
66-
except BOTO_BASE_ERRORS:
66+
except Exception:
6767
if not self.fail_silently:
6868
raise
6969
else:
@@ -75,6 +75,22 @@ def close(self):
7575
self.client.close()
7676
self.client = None
7777

78+
def _send(self, message):
79+
if self.client:
80+
return super()._send(message)
81+
elif self.fail_silently:
82+
# (Probably missing boto3 credentials in open().)
83+
return False
84+
else:
85+
class_name = self.__class__.__name__
86+
raise RuntimeError(
87+
"boto3 Session has not been opened in {class_name}._send. "
88+
"(This is either an implementation error in {class_name}, "
89+
"or you are incorrectly calling _send directly.)".format(
90+
class_name=class_name
91+
)
92+
)
93+
7894
def build_message_payload(self, message, defaults):
7995
if getattr(message, "template_id", UNSET) is not UNSET:
8096
# For simplicity, use SESv2 SendBulkEmail for all templated messages

Diff for: anymail/backends/base_requests.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ def close(self):
4747
self.session = None
4848

4949
def _send(self, message):
50-
if self.session is None:
50+
if self.session:
51+
return super()._send(message)
52+
elif self.fail_silently:
53+
# create_session failed
54+
return False
55+
else:
5156
class_name = self.__class__.__name__
5257
raise RuntimeError(
5358
"Session has not been opened in {class_name}._send. "
@@ -56,7 +61,6 @@ def _send(self, message):
5661
class_name=class_name
5762
)
5863
)
59-
return super()._send(message)
6064

6165
def create_session(self):
6266
"""Create the requests.Session object used by this instance of the backend.

Diff for: tests/test_amazon_ses_backend.py

+10
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,16 @@ def test_api_failure_fail_silently(self):
393393
sent = self.message.send(fail_silently=True)
394394
self.assertEqual(sent, 0)
395395

396+
def test_session_failure_fail_silently(self):
397+
# Make sure fail_silently is respected if boto3.Session creation fails
398+
# (e.g., due to invalid or missing credentials)
399+
from botocore.exceptions import NoCredentialsError
400+
401+
self.mock_session.side_effect = NoCredentialsError()
402+
403+
sent = self.message.send(fail_silently=True)
404+
self.assertEqual(sent, 0)
405+
396406
def test_prevents_header_injection(self):
397407
# Since we build the raw MIME message, we're responsible for preventing header
398408
# injection. django.core.mail.EmailMessage.message() implements most of that

Diff for: tests/test_amazon_sesv2_backend.py

+10
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,16 @@ def test_api_failure_fail_silently(self):
403403
sent = self.message.send(fail_silently=True)
404404
self.assertEqual(sent, 0)
405405

406+
def test_session_failure_fail_silently(self):
407+
# Make sure fail_silently is respected if boto3.Session creation fails
408+
# (e.g., due to invalid or missing credentials)
409+
from botocore.exceptions import NoCredentialsError
410+
411+
self.mock_session.side_effect = NoCredentialsError()
412+
413+
sent = self.message.send(fail_silently=True)
414+
self.assertEqual(sent, 0)
415+
406416
def test_prevents_header_injection(self):
407417
# Since we build the raw MIME message, we're responsible for preventing header
408418
# injection. django.core.mail.EmailMessage.message() implements most of that

Diff for: tests/test_base_backends.py

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest import mock
2+
13
from django.test import SimpleTestCase, override_settings, tag
24

35
from anymail.backends.base_requests import AnymailRequestsBackend, RequestsPayload
@@ -69,6 +71,14 @@ def test_timeout_setting(self):
6971
timeout = self.get_api_call_arg("timeout")
7072
self.assertEqual(timeout, 5)
7173

74+
@mock.patch(f"{__name__}.MinimalRequestsBackend.create_session")
75+
def test_create_session_error_fail_silently(self, mock_create_session):
76+
# If create_session fails and fail_silently is True,
77+
# make sure _send doesn't raise a misleading error.
78+
mock_create_session.side_effect = ValueError("couldn't create session")
79+
sent = self.message.send(fail_silently=True)
80+
self.assertEqual(sent, 0)
81+
7282

7383
@tag("live")
7484
@override_settings(EMAIL_BACKEND="tests.test_base_backends.MinimalRequestsBackend")

0 commit comments

Comments
 (0)