Skip to content

Commit

Permalink
Fix support for Django native email and add tracking console backend
Browse files Browse the repository at this point in the history
  • Loading branch information
codingjoe authored and amureki committed Jul 21, 2023
1 parent 861d8a7 commit d8989c0
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 24 deletions.
82 changes: 61 additions & 21 deletions emark/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
2 changes: 1 addition & 1 deletion emark/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
],
Expand Down
2 changes: 1 addition & 1 deletion emark/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 5 additions & 1 deletion emark/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)


Expand Down
75 changes: 75 additions & 0 deletions tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html></html>", "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("<html></html>", "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):
Expand All @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="<html></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"

Expand Down

0 comments on commit d8989c0

Please sign in to comment.