diff --git a/sendgrid_backend/mail.py b/sendgrid_backend/mail.py index c2c6583..73dadef 100644 --- a/sendgrid_backend/mail.py +++ b/sendgrid_backend/mail.py @@ -1,3 +1,5 @@ +import logging + from future.builtins import str import base64 from email.mime.base import MIMEBase @@ -21,6 +23,8 @@ from python_http_client.exceptions import HTTPError +from sendgrid_backend.signals import sendgrid_email_sent + SENDGRID_VERSION = sendgrid.__version__ # Need to change imports because of breaking changes in sendgrid's v6 api @@ -34,6 +38,8 @@ if sys.version_info >= (3.0, 0.0): basestring = str +logger = logging.getLogger(__name__) + class SendgridBackend(BaseEmailBackend): """ @@ -118,16 +124,23 @@ def send_messages(self, email_messages): for msg in email_messages: data = self._build_sg_mail(msg) + fail_flag = True try: resp = self.sg.client.mail.send.post(request_body=data) msg.extra_headers['status'] = resp.status_code x_message_id = resp.headers.get('x-message-id', None) if x_message_id: msg.extra_headers['message_id'] = x_message_id + else: + logger.warning('No x_message_id header received from sendgrid api') success += 1 - except HTTPError: + fail_flag = False + except HTTPError as e: + logger.error("Failed to send email %s" % e) if not self.fail_silently: raise + finally: + sendgrid_email_sent.send(sender=self.__class__, message=msg, fail_flag=fail_flag) return success def _create_sg_attachment(self, django_attch): @@ -188,7 +201,6 @@ def _build_sg_mail(self, msg): mail = Mail() mail.from_email = Email(*self._parse_email_address(msg.from_email)) - mail.subject = msg.subject personalization = Personalization() for addr in msg.to: @@ -204,7 +216,13 @@ def _build_sg_mail(self, msg): for k, v in msg.custom_args.items(): personalization.add_custom_arg(CustomArg(k, v)) - personalization.subject = msg.subject + if self._is_transaction_template(msg): + if msg.subject: + logger.warning("Message subject is ignored in transactional template, " + "please add it as template variable (e.g. {{ subject }}") + # See https://github.com/sendgrid/sendgrid-nodejs/issues/843 + else: + personalization.subject = msg.subject for k, v in msg.extra_headers.items(): if k.lower() == "reply-to": @@ -212,14 +230,6 @@ def _build_sg_mail(self, msg): else: personalization.add_header(Header(k, v)) - if hasattr(msg, "template_id"): - mail.template_id = msg.template_id - if hasattr(msg, "substitutions"): - for k, v in msg.substitutions.items(): - personalization.add_substitution(Substitution(k, v)) - if hasattr(msg, "dynamic_template_data"): - personalization.dynamic_template_data = msg.dynamic_template_data - if hasattr(msg, "ip_pool_name"): if not isinstance(msg.ip_pool_name, basestring): raise ValueError( @@ -249,8 +259,6 @@ def _build_sg_mail(self, msg): type(msg.send_at))) personalization.send_at = msg.send_at - mail.add_personalization(personalization) - if hasattr(msg, "reply_to") and msg.reply_to: if mail.reply_to: # If this code path is triggered, the reply_to on the sg mail was set in a header above @@ -270,18 +278,39 @@ def _build_sg_mail(self, msg): sg_attch = self._create_sg_attachment(attch) mail.add_attachment(sg_attch) - msg.body = ' ' if msg.body == '' else msg.body - - if isinstance(msg, EmailMultiAlternatives): - mail.add_content(Content("text/plain", msg.body)) - for alt in msg.alternatives: - if alt[1] == "text/html": - mail.add_content(Content(alt[1], alt[0])) - elif msg.content_subtype == "html": - mail.add_content(Content("text/plain", " ")) - mail.add_content(Content("text/html", msg.body)) + if self._is_transaction_template(msg): + if msg.body: + logger.warning("Message body is ignored in transactional template") else: - mail.add_content(Content("text/plain", msg.body)) + msg.body = ' ' if msg.body == '' else msg.body + + if hasattr(msg, "template_id"): + # Template mails should not have subject and content attributes + mail.template_id = msg.template_id + if hasattr(msg, "substitutions"): + for k, v in msg.substitutions.items(): + personalization.add_substitution(Substitution(k, v)) + if hasattr(msg, "dynamic_template_data"): + if SENDGRID_VERSION < "6": + logger.warning("dynamic_template_data not available in sendgrid version < 6") + personalization.dynamic_template_data = msg.dynamic_template_data + + if not self._is_transaction_template(msg): + # In sendgrid v6 we should not specify subject and content between request parameter + # when we are sending a request for a transactional template + mail.subject = msg.subject + if isinstance(msg, EmailMultiAlternatives): + mail.add_content(Content("text/plain", msg.body)) + for alt in msg.alternatives: + if alt[1] == "text/html": + mail.add_content(Content(alt[1], alt[0])) + elif msg.content_subtype == "html": + mail.add_content(Content("text/plain", " ")) + mail.add_content(Content("text/html", msg.body)) + else: + mail.add_content(Content("text/plain", msg.body)) + + mail.add_personalization(personalization) if hasattr(msg, "categories"): for cat in msg.categories: @@ -307,3 +336,6 @@ def _build_sg_mail(self, msg): mail.tracking_settings = tracking_settings return mail.get() + + def _is_transaction_template(self, msg): + return SENDGRID_VERSION >= "6" and hasattr(msg, "template_id") diff --git a/sendgrid_backend/signals.py b/sendgrid_backend/signals.py new file mode 100644 index 0000000..25c048c --- /dev/null +++ b/sendgrid_backend/signals.py @@ -0,0 +1,3 @@ +import django.dispatch + +sendgrid_email_sent = django.dispatch.Signal(providing_args=["message", "fail_flag"]) diff --git a/sendgrid_backend/version.py b/sendgrid_backend/version.py index 777f190..3e2f46a 100644 --- a/sendgrid_backend/version.py +++ b/sendgrid_backend/version.py @@ -1 +1 @@ -__version__ = "0.8.0" +__version__ = "0.9.0" diff --git a/setup.py b/setup.py index 9df6bff..7ccb299 100644 --- a/setup.py +++ b/setup.py @@ -7,9 +7,13 @@ with open(path.join(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() +__version__ = None +with open('sendgrid_backend/version.py') as f: + exec(f.read()) + setup( name="django-sendgrid-v5", - version="0.8.1", + version=str(__version__), description="An implementation of Django's EmailBackend compatible with sendgrid-python v5+", long_description=long_description, url="https://github.com/sklarsa/django-sendgrid-v5", diff --git a/test/test_mail.py b/test/test_mail.py index de0c68e..3fed88f 100644 --- a/test/test_mail.py +++ b/test/test_mail.py @@ -341,7 +341,7 @@ def test_mime(self): self.assertEqual(result["attachments"][0]["content"], base64.b64encode(f.read())) self.assertEqual(result["attachments"][0]["type"], "image/png") - def test_templating(self): + def test_templating_sendgrid_v5(self): msg = EmailMessage( subject="Hello, World!", body="Hello, World!", @@ -354,6 +354,43 @@ def test_templating(self): self.assertIn("template_id", result) self.assertEquals(result["template_id"], "test_template") + def test_templating_sendgrid(self): + if SENDGRID_VERSION < "6": + msg = EmailMessage( + subject="Hello, World!", + body="Hello, World!", + from_email="Sam Smith ", + to=["John Doe ", "jane.doe@example.com"], + ) + msg.template_id = "test_template" + result = self.backend._build_sg_mail(msg) + + self.assertIn("template_id", result) + self.assertEquals(result["template_id"], "test_template") + # Testing that for sendgrid v5 the code behave in the same way + self.assertEquals(result["content"], [{"type": "text/plain", "value": "Hello, World!"}]) + self.assertEquals(result["subject"], "Hello, World!") + self.assertEquals(result["personalizations"][0]["subject"], "Hello, World!") + else: + msg = EmailMessage( + from_email="Sam Smith ", + to=["John Doe ", "jane.doe@example.com"], + ) + msg.template_id = "test_template" + msg.dynamic_template_data = { + "subject": "Hello, World!", + "content": "Hello, World!", + "link": "http://hello.com" + } + result = self.backend._build_sg_mail(msg) + + self.assertIn("template_id", result) + self.assertEquals(result["template_id"], "test_template") + self.assertEquals(result["personalizations"][0]["dynamic_template_data"], msg.dynamic_template_data) + # Subject and content should not be between request param + self.assertNotIn("subject", result) + self.assertNotIn("content", result) + def test_asm(self): msg = EmailMessage( subject="Hello, World!",