diff --git a/emark/backends.py b/emark/backends.py index 009bd1d..957369e 100644 --- a/emark/backends.py +++ b/emark/backends.py @@ -8,8 +8,49 @@ from django.core.mail.message import sanitize_address from emark import models +from emark.message import MarkdownEmail -__all__ = ["ConsoleEmailBackend", "TrackingSMTPEmailBackend"] +__all__ = [ + "ConsoleEmailBackend", + "TrackingConsoleEmailBackend", + "TrackingSMTPEmailBackend", +] + + +class TrackingEmailBackendMixin: + """Add tracking framework to an email backend.""" + + def send_messages(self, email_messages): + self._messages_sent = [] + try: + return super().send_messages(email_messages) + finally: + models.Send.objects.bulk_create(self._messages_sent) + + def _track_message_clone(self, clone, message): + if isinstance(clone, MarkdownEmail): + self._messages_sent.append( + models.Send( + pk=clone._tracking_uuid, + from_address=message["From"], + to_address=message["To"], + subject=message["Subject"], + body=clone.body, + html=clone.html, + user=getattr(clone, "user", None), + utm=clone.get_utm_params(**clone.utm_params), + ) + ) + else: + self._messages_sent.append( + models.Send( + pk=clone._tracking_uuid, + from_address=message["From"], + to_address=message["To"], + subject=message["Subject"], + body=clone.body, + ) + ) class ConsoleEmailBackend(_EmailBackend): @@ -32,8 +73,25 @@ def write_message(self, message): f"{payload_count - 1} more attachment(s) have been omitted.\n" ) + return msg + + +class TrackingConsoleEmailBackend(TrackingEmailBackendMixin, ConsoleEmailBackend): + """Like the console email backend but with click and open tracking.""" -class TrackingSMTPEmailBackend(_SMTPEmailBackend): + def write_message(self, message): + for recipient in message.recipients(): + clone = copy.copy(message) + clone.to = [recipient] + clone.cc = [] + clone.bcc = [] + # enable tracking + clone._tracking_uuid = uuid.uuid4() + msg = super().write_message(clone) + self._track_message_clone(clone, msg) + + +class TrackingSMTPEmailBackend(TrackingEmailBackendMixin, _SMTPEmailBackend): """ Like the SMTP email backend but with click and open tracking. @@ -42,13 +100,6 @@ class TrackingSMTPEmailBackend(_SMTPEmailBackend): email is sent individually to each address. """ - def send_messages(self, email_messages): - self._messages_sent = [] - try: - return super().send_messages(email_messages) - finally: - models.Send.objects.bulk_create(self._messages_sent) - def _send(self, email_message): for recipient in email_message.recipients(): clone = copy.copy(email_message) @@ -71,16 +122,5 @@ def _send(self, email_message): raise return False else: - self._messages_sent.append( - models.Send( - pk=clone._tracking_uuid, - from_address=from_email, - to_address=recipient, - subject=message["Subject"], - body=clone.body, - html=clone.html, - user=getattr(clone, "user", None), - utm=clone.get_utm_params(**clone.utm_params), - ) - ) + self._track_message_clone(clone, message) return True diff --git a/emark/migrations/0001_initial.py b/emark/migrations/0001_initial.py index db54df0..c73f756 100644 --- a/emark/migrations/0001_initial.py +++ b/emark/migrations/0001_initial.py @@ -39,7 +39,7 @@ class Migration(migrations.Migration): ("to_address", models.EmailField(max_length=254)), ("subject", models.TextField(max_length=998)), ("body", models.TextField()), - ("html", models.TextField()), + ("html", models.TextField(null=True)), ("utm", models.JSONField(default=dict)), ("created_at", models.DateTimeField(auto_now_add=True)), ], diff --git a/emark/models.py b/emark/models.py index 7f70420..08558b7 100644 --- a/emark/models.py +++ b/emark/models.py @@ -22,7 +22,7 @@ class Send(models.Model): to_address = models.EmailField() subject = models.TextField(max_length=998) # RFC 2822 body = models.TextField() - html = models.TextField() + html = models.TextField(null=True) utm = models.JSONField(default=dict) created_at = models.DateTimeField(auto_now_add=True) diff --git a/emark/views.py b/emark/views.py index c1aa079..26ebaa3 100644 --- a/emark/views.py +++ b/emark/views.py @@ -24,8 +24,12 @@ class EmailDetailView(SingleObjectMixin, View): def get(self, request, *args, **kwargs): self.object = self.get_object() + if self.object.html: + return http.HttpResponse( + self.object.html.encode(), status=200, content_type="text/html" + ) return http.HttpResponse( - self.object.html.encode(), status=200, content_type="text/html" + self.object.body.encode(), status=200, content_type="text/plain" ) diff --git a/tests/test_backends.py b/tests/test_backends.py index 389afbd..5700bba 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -20,6 +20,61 @@ def test_write_message(self): assert "1 more attachment(s) have been omitted." in stdout +class TestTrackingConsoleEmailBackend: + @pytest.mark.django_db + def test_send(self, email_message): + email_message.to = [ + "peter.parker@avengers.com", + "dr.strange@avengers.com", + ] + email_message.cc = ["t-dog@avengers.com"] + + with io.StringIO() as stream: + backend = backends.TrackingConsoleEmailBackend(stream=stream) + assert backend.send_messages([email_message]) == 1 + assert Send.objects.count() == 3 + obj = Send.objects.get(to_address="peter.parker@avengers.com") + assert str(obj.uuid) in obj.body + + @pytest.mark.django_db + def test_send__with_user(self, admin_user, email_message): + email_message.to = [admin_user.email] + email_message.user = admin_user + + with io.StringIO() as stream: + backend = backends.TrackingConsoleEmailBackend(stream=stream) + assert backend.send_messages([email_message]) == 1 + + assert Send.objects.count() == 1 + obj = Send.objects.get(to_address=admin_user.email) + assert obj.user == admin_user + + @pytest.mark.django_db + def test_write_message__native_email(self): + msg = EmailMultiAlternatives(to=["ironman@avengers.com"], body="foo") + msg.attach_alternative("", "text/html") + with io.StringIO() as stream: + backends.TrackingConsoleEmailBackend(stream=stream).send_messages([msg]) + stdout = stream.getvalue() + assert "html" not in stdout + assert "1 more attachment(s) have been omitted." in stdout + assert Send.objects.count() == 1 + + @pytest.mark.django_db + def test_write_message__native_email__multiple_receipients(self): + msg = EmailMultiAlternatives( + to=["spiderman@avengers.com"], cc=["peter.parker@aol.com"], body="foo" + ) + msg.attach_alternative("", "text/html") + with io.StringIO() as stream: + backends.TrackingConsoleEmailBackend(stream=stream).send_messages([msg]) + stdout = stream.getvalue() + assert "To: peter.parker@aol.com" in stdout + assert "To: spiderman@avengers.com" in stdout + assert "Cc:" not in stdout + assert Send.objects.count() == 2 + + class TestTrackingSMTPEmailBackend: @pytest.mark.django_db def test_send(self, email_message): @@ -40,6 +95,26 @@ class TestBackend(backends.TrackingSMTPEmailBackend): obj = Send.objects.get(to_address="peter.parker@avengers.com") assert str(obj.uuid) in obj.body + @pytest.mark.django_db + def test_send__native_email(self): + email_message = EmailMultiAlternatives( + to=[ + "peter.parker@avengers.com", + "dr.strange@avengers.com", + ], + cc=["t-dog@avengers.com"], + body="foo", + ) + + class TestBackend(backends.TrackingSMTPEmailBackend): + connection_class = MagicMock + + backend = TestBackend(fail_silently=False) + backend.connection = Mock() + assert backend.send_messages([email_message]) == 1 + assert backend.connection.sendmail.call_count == 3 + assert Send.objects.count() == 3 + @pytest.mark.django_db def test_send__with_user(self, admin_user, email_message): email_message.to = [admin_user.email] diff --git a/tests/test_views.py b/tests/test_views.py index 5d82bf6..80bc896 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -14,6 +14,14 @@ def test_get(self, client): msg = baker.make("emark.Send") response = client.get(msg.get_absolute_url()) assert response.status_code == 200 + assert response.content == msg.body.encode("utf-8") + assert response["Content-Type"] == "text/plain" + + @pytest.mark.django_db + def test_get__html(self, client): + msg = baker.make("emark.Send", html="") + response = client.get(msg.get_absolute_url()) + assert response.status_code == 200 assert response.content == msg.html.encode("utf-8") assert response["Content-Type"] == "text/html"