From 9c8a36da138b637d3f685badf4bfd52b4d024af8 Mon Sep 17 00:00:00 2001 From: Charlie DeTar Date: Wed, 24 Jan 2018 12:17:10 -0700 Subject: [PATCH 1/3] Un-hardcode message ID in test backend `EmailMultiAlternatives.message()` serializes an email message for sending, and among other things adds a unique "Message-ID" header if one isn't already present. Use this in place of a hard-coded message ID in the test backend. For tests that inspect the message ID in the `anymail_status`, provide an explicit "Message-ID" header rather than using the ephemeral generated one. --- anymail/backends/test.py | 5 ++++- tests/test_send_signals.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/anymail/backends/test.py b/anymail/backends/test.py index 41f900bd..d3dd95ce 100644 --- a/anymail/backends/test.py +++ b/anymail/backends/test.py @@ -41,7 +41,10 @@ def post_to_esp(self, payload, message): raise response except AttributeError: # Default is to return 'sent' for each recipient - status = AnymailRecipientStatus(message_id=1, status='sent') + status = AnymailRecipientStatus( + message_id=message.message()['Message-ID'], + status='sent' + ) response = { 'recipient_status': {email: status for email in payload.recipient_emails} } diff --git a/tests/test_send_signals.py b/tests/test_send_signals.py index b416c144..5cf949fe 100644 --- a/tests/test_send_signals.py +++ b/tests/test_send_signals.py @@ -59,20 +59,23 @@ class TestPostSendSignal(TestBackendTestCase): def test_post_send(self): """Post-send receiver called for each message, after sending""" + message_id = 'asdf' + @receiver(post_send, weak=False) def handle_post_send(sender, message, status, esp_name, **kwargs): self.assertEqual(self.get_send_count(), 1) # already sent self.assertEqual(sender, TestEmailBackend) self.assertEqual(message, self.message) self.assertEqual(status.status, {'sent'}) - self.assertEqual(status.message_id, 1) # TestEmailBackend default message_id + self.assertEqual(status.message_id, message_id) self.assertEqual(status.recipients['to@example.com'].status, 'sent') - self.assertEqual(status.recipients['to@example.com'].message_id, 1) + self.assertEqual(status.recipients['to@example.com'].message_id, message_id) self.assertEqual(esp_name, "Test") # the TestEmailBackend's ESP is named "Test" self.receiver_called = True self.addCleanup(post_send.disconnect, receiver=handle_post_send) self.receiver_called = False + self.message.extra_headers['Message-ID'] = message_id self.message.send() self.assertTrue(self.receiver_called) From edf4f606ae13ac6ef54a6d120869589455e49093 Mon Sep 17 00:00:00 2001 From: Charlie DeTar Date: Wed, 24 Jan 2018 14:42:44 -0700 Subject: [PATCH 2/3] Add a console backend for use in development Adds an EmailBackend derived from both Anymail's test backend and Django's console backend, to provide anymail statuses and signal handling while printing messages to the console. For use during development on localhost. --- anymail/backends/console.py | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 anymail/backends/console.py diff --git a/anymail/backends/console.py b/anymail/backends/console.py new file mode 100644 index 00000000..842a81a4 --- /dev/null +++ b/anymail/backends/console.py @@ -0,0 +1,38 @@ +from django.core.mail.backends.console import EmailBackend as DjangoConsoleBackend + +from ..exceptions import AnymailError +from .test import EmailBackend as AnymailTestBackend + + +class EmailBackend(AnymailTestBackend, DjangoConsoleBackend): + """ + Anymail backend that prints messages to the console, while retaining + anymail statuses and signals. + """ + + esp_name = "Console" + + def send_messages(self, email_messages): + if not email_messages: + return + msg_count = 0 + with self._lock: + try: + stream_created = self.open() + for message in email_messages: + try: + sent = self._send(message) + except AnymailError: + if self.fail_silently: + sent = False + else: + raise + if sent: + self.write_message(message) + self.stream.flush() # flush after each message + msg_count += 1 + finally: + if stream_created: + self.close() + + return msg_count From 065ff92878b9bcb35719494260bf553b0ec4ffa4 Mon Sep 17 00:00:00 2001 From: Charlie DeTar Date: Sun, 28 Jan 2018 12:16:02 -0700 Subject: [PATCH 3/3] Different message ID strategies for console, test For the test EmailBackend, get message ID's based on array position in `mail.outbox`, so that tests can predict the message ID. For the console EmailBackend, use a uuid4 so that message ID's will be globally unique. --- anymail/backends/console.py | 5 +++++ anymail/backends/test.py | 7 ++++++- tests/test_send_signals.py | 7 ++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/anymail/backends/console.py b/anymail/backends/console.py index 842a81a4..5fa9ffed 100644 --- a/anymail/backends/console.py +++ b/anymail/backends/console.py @@ -1,3 +1,4 @@ +import uuid from django.core.mail.backends.console import EmailBackend as DjangoConsoleBackend from ..exceptions import AnymailError @@ -12,6 +13,10 @@ class EmailBackend(AnymailTestBackend, DjangoConsoleBackend): esp_name = "Console" + def get_esp_message_id(self, message): + # Generate a guaranteed-unique ID for the message + return str(uuid.uuid4()) + def send_messages(self, email_messages): if not email_messages: return diff --git a/anymail/backends/test.py b/anymail/backends/test.py index d3dd95ce..b78b7d08 100644 --- a/anymail/backends/test.py +++ b/anymail/backends/test.py @@ -27,6 +27,11 @@ def __init__(self, *args, **kwargs): if not hasattr(mail, 'outbox'): mail.outbox = [] # see django.core.mail.backends.locmem + def get_esp_message_id(self, message): + # Get a unique ID for the message. The message must have been added to + # the outbox first. + return mail.outbox.index(message) + def build_message_payload(self, message, defaults): return TestPayload(backend=self, message=message, defaults=defaults) @@ -42,7 +47,7 @@ def post_to_esp(self, payload, message): except AttributeError: # Default is to return 'sent' for each recipient status = AnymailRecipientStatus( - message_id=message.message()['Message-ID'], + message_id=self.get_esp_message_id(message), status='sent' ) response = { diff --git a/tests/test_send_signals.py b/tests/test_send_signals.py index 5cf949fe..dddd5268 100644 --- a/tests/test_send_signals.py +++ b/tests/test_send_signals.py @@ -59,23 +59,20 @@ class TestPostSendSignal(TestBackendTestCase): def test_post_send(self): """Post-send receiver called for each message, after sending""" - message_id = 'asdf' - @receiver(post_send, weak=False) def handle_post_send(sender, message, status, esp_name, **kwargs): self.assertEqual(self.get_send_count(), 1) # already sent self.assertEqual(sender, TestEmailBackend) self.assertEqual(message, self.message) self.assertEqual(status.status, {'sent'}) - self.assertEqual(status.message_id, message_id) + self.assertEqual(status.message_id, 0) self.assertEqual(status.recipients['to@example.com'].status, 'sent') - self.assertEqual(status.recipients['to@example.com'].message_id, message_id) + self.assertEqual(status.recipients['to@example.com'].message_id, 0) self.assertEqual(esp_name, "Test") # the TestEmailBackend's ESP is named "Test" self.receiver_called = True self.addCleanup(post_send.disconnect, receiver=handle_post_send) self.receiver_called = False - self.message.extra_headers['Message-ID'] = message_id self.message.send() self.assertTrue(self.receiver_called)