Skip to content

Commit

Permalink
Fix connection issues
Browse files Browse the repository at this point in the history
Render markdown emails before opening a SMTP connection to avoid
connection timeouts. Services like AWS SES have very short
connection timeouts. Therefore, all messages should be fully
rendered before a connection is established.
  • Loading branch information
codingjoe committed Jul 28, 2023
1 parent 3c9b14e commit 49e2a02
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 173 deletions.
117 changes: 63 additions & 54 deletions emark/backends.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,78 @@
import copy
import smtplib
import uuid

from django.conf import settings
from django.core.mail import EmailMessage
from django.core.mail.backends.console import EmailBackend as _EmailBackend
from django.core.mail.backends.smtp import EmailBackend as _SMTPEmailBackend
from django.core.mail.message import sanitize_address

from emark import models
from emark.message import MarkdownEmail

__all__ = [
"ConsoleEmailBackend",
"SMTPEmailBackend",
"TrackingConsoleEmailBackend",
"TrackingSMTPEmailBackend",
]


class RenderEmailBackendMixin:
def send_messages(self, email_messages):
self._messages_sent = []
for message in email_messages:
if isinstance(message, MarkdownEmail):
message.render()
try:
return super().send_messages(email_messages)
finally:
models.Send.objects.bulk_create(self._messages_sent)


class TrackingEmailBackendMixin:
"""Add tracking framework to an email backend."""

def send_messages(self, email_messages):
self._messages_sent = []
for message in email_messages:
if isinstance(message, MarkdownEmail):
message.render(tracking_uuid=uuid.uuid4())
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):
def _track_message(self, message: EmailMessage):
if isinstance(message, 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),
pk=message.uuid,
from_email=message.from_email,
to=message.to,
cc=message.cc,
bcc=message.bcc,
reply_to=message.reply_to,
subject=message.subject,
body=message.body,
html=message.html,
user=getattr(message, "user", None),
utm=message.get_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,
from_email=message.from_email,
to=message.to,
cc=message.cc,
bcc=message.bcc,
reply_to=message.reply_to,
subject=message.subject,
body=message.body,
)
)


class ConsoleEmailBackend(_EmailBackend):
"""Like the console email backend but only with the plain text body."""
class ConsoleEmailBackendMixin:
"""Drop email alternative parts and attachments for the console backend."""

def write_message(self, message):
msg = message.message()
Expand All @@ -76,19 +94,26 @@ def write_message(self, message):
return msg


class TrackingConsoleEmailBackend(TrackingEmailBackendMixin, ConsoleEmailBackend):
class ConsoleEmailBackend(
RenderEmailBackendMixin, ConsoleEmailBackendMixin, _EmailBackend
):
"""Like the console email backend but only with the plain text body."""


class SMTPEmailBackend(RenderEmailBackendMixin, _SMTPEmailBackend):
"""SMTP email backend that renders messages before establishing an SMTP transport."""

pass


class TrackingConsoleEmailBackend(
TrackingEmailBackendMixin, ConsoleEmailBackendMixin, _EmailBackend
):
"""Like the console email backend but with click and open tracking."""

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)
self._track_message(message)
return super().write_message(message)


class TrackingSMTPEmailBackend(TrackingEmailBackendMixin, _SMTPEmailBackend):
Expand All @@ -101,26 +126,10 @@ class TrackingSMTPEmailBackend(TrackingEmailBackendMixin, _SMTPEmailBackend):
"""

def _send(self, email_message):
for recipient in email_message.recipients():
clone = copy.copy(email_message)
clone.to = [recipient]
clone.cc = []
clone.bcc = []
# enable tracking
clone._tracking_uuid = uuid.uuid4()

encoding = clone.encoding or settings.DEFAULT_CHARSET
from_email = sanitize_address(clone.from_email, encoding)
recipients = [sanitize_address(recipient, encoding)]
message = clone.message()
try:
self.connection.sendmail(
from_email, recipients, message.as_bytes(linesep="\r\n")
)
except smtplib.SMTPException:
if not self.fail_silently:
raise
return False
else:
self._track_message_clone(clone, message)
return True
sent = False
try:
sent = super()._send(email_message)
return sent
finally:
if sent:
self._track_message(email_message)
Loading

0 comments on commit 49e2a02

Please sign in to comment.