Skip to content

Commit 54b3226

Browse files
authored
Refactors Personalizations logic to simplify flow (#93)
EmailMessage.personalizations can now accept a Sendgrid API object or python dict
1 parent 8498dca commit 54b3226

File tree

4 files changed

+188
-45
lines changed

4 files changed

+188
-45
lines changed

sendgrid_backend/mail.py

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import uuid
99
import warnings
1010
from email.mime.base import MIMEBase
11-
from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple, Union
11+
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union
1212

1313
from django.conf import settings
1414
from django.core.exceptions import ImproperlyConfigured
@@ -34,7 +34,12 @@
3434
)
3535

3636
from sendgrid_backend.signals import sendgrid_email_sent
37-
from sendgrid_backend.util import SENDGRID_5, SENDGRID_6, get_django_setting
37+
from sendgrid_backend.util import (
38+
SENDGRID_5,
39+
SENDGRID_6,
40+
dict_to_personalization,
41+
get_django_setting,
42+
)
3843

3944
DjangoAttachment = Union[Tuple[str, Union[bytes, str], str], MIMEBase]
4045

@@ -242,10 +247,10 @@ def _parse_email_address(self, address: str) -> Tuple[str, Optional[str]]:
242247

243248
def _build_sg_personalization(
244249
self,
245-
to: Iterable[str],
250+
to: List[Any],
246251
msg: EmailMessage,
247252
extra_headers: Iterable[Header],
248-
personalizations: Dict = None,
253+
existing_personalizations: Optional[Personalization] = None,
249254
) -> Personalization:
250255
"""
251256
Constructs a Sendgrid Personalization instance / row for the given recipients.
@@ -257,41 +262,44 @@ def _build_sg_personalization(
257262
to: The email addresses for the given personalization.
258263
msg: The base Django Email message object - used to fill (missing) personalization data
259264
extra_headers: The non "reply-to" headers for the personalization.
260-
personalizations: Personalization data, eg. dynamic_template_data or substitutions.
265+
existing_personalizations: Personalization data, eg. dynamic_template_data or substitutions.
261266
A given value should have key equivalent to corresponding msg attr
262267
263268
264269
Returns:
265270
A sendgrid personalization instance
266271
"""
272+
personalization = existing_personalizations or Personalization()
267273

268-
personalizations = personalizations or {}
269-
personalization = Personalization()
270-
271-
for addr in msg.to:
272-
personalization.add_to(Email(*self._parse_email_address(addr)))
274+
if to:
275+
if type(to[0]) == str:
276+
for addr in to:
277+
personalization.add_to(Email(*self._parse_email_address(addr)))
278+
else:
279+
personalization.tos = to
273280

274-
for addr in personalizations.get("cc") or msg.cc:
275-
personalization.add_cc(Email(*self._parse_email_address(addr)))
281+
if not personalization.ccs:
282+
for addr in msg.cc:
283+
personalization.add_cc(Email(*self._parse_email_address(addr)))
276284

277-
for addr in personalizations.get("bcc") or msg.bcc:
278-
personalization.add_bcc(Email(*self._parse_email_address(addr)))
285+
if not personalization.bccs:
286+
for addr in msg.bcc:
287+
personalization.add_bcc(Email(*self._parse_email_address(addr)))
279288

280-
for k, v in personalizations.get(
281-
"custom_args",
282-
getattr(msg, "custom_args", {}),
283-
).items():
284-
personalization.add_custom_arg(CustomArg(k, v))
289+
if not personalization.custom_args:
290+
for k, v in getattr(msg, "custom_args", {}).items():
291+
personalization.add_custom_arg(CustomArg(k, v))
285292

286293
if self._is_transaction_template(msg):
287-
if msg.subject:
294+
if personalization.subject or msg.subject:
288295
logger.warning(
289296
"Message subject is ignored in transactional template, "
290297
"please add it as template variable (e.g. {{ subject }}"
291298
)
292299
# See https://github.com/sendgrid/sendgrid-nodejs/issues/843
293-
else:
294-
personalization.subject = personalizations.get("subject") or msg.subject
300+
301+
if not personalization.subject:
302+
personalization.subject = msg.subject
295303

296304
for header in extra_headers:
297305
personalization.add_header(header)
@@ -308,15 +316,12 @@ def _build_sg_personalization(
308316
personalization.send_at = msg.send_at
309317

310318
if hasattr(msg, "template_id"):
311-
for k, v in personalizations.get(
312-
"substitutions",
313-
getattr(msg, "substitutions", {}),
314-
).items():
315-
personalization.add_substitution(Substitution(k, v))
316-
317-
dtd = personalizations.get(
318-
"dynamic_template_data",
319-
getattr(msg, "dynamic_template_data", None),
319+
if not personalization.substitutions:
320+
for k, v in getattr(msg, "substitutions", {}).items():
321+
personalization.add_substitution(Substitution(k, v))
322+
323+
dtd = personalization.dynamic_template_data or getattr(
324+
msg, "dynamic_template_data", None
320325
)
321326
if dtd:
322327
if SENDGRID_5:
@@ -420,13 +425,17 @@ def _build_sg_mail(self, msg: EmailMessage) -> Dict:
420425

421426
if hasattr(msg, "personalizations"):
422427
for personalization in msg.personalizations:
423-
to = personalization.pop("to")
428+
if type(personalization) == Dict:
429+
personalization = dict_to_personalization(personalization)
430+
431+
assert type(personalization) == Personalization
432+
424433
mail.add_personalization(
425434
self._build_sg_personalization(
426-
to,
435+
personalization.tos or msg.to,
427436
msg,
428437
personalization_headers,
429-
personalizations=personalization,
438+
existing_personalizations=personalization,
430439
)
431440
)
432441
elif getattr(msg, "make_private", False):

sendgrid_backend/util.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from typing import Any, Dict
2+
13
import sendgrid
24
from django.conf import settings
5+
from sendgrid.helpers.mail import Personalization
36

47
SENDGRID_VERSION = sendgrid.__version__
58

@@ -14,3 +17,30 @@ def get_django_setting(setting_str, default=None):
1417
if hasattr(settings, setting_str):
1518
return getattr(settings, setting_str, default)
1619
return default
20+
21+
22+
def dict_to_personalization(data: Dict[Any, Any]) -> Personalization:
23+
"""
24+
Reverses Sendgrid's Personalization.get() method to create a Personalization
25+
object from its emitted data structure (in the form of a Dict)
26+
"""
27+
personalization = Personalization()
28+
29+
properties = [
30+
p
31+
for p in dir(Personalization)
32+
if isinstance(getattr(Personalization, p), property)
33+
]
34+
for attr in properties:
35+
if attr in ["tos", "ccs", "bccs"]:
36+
key = attr[:-1]
37+
else:
38+
key = attr
39+
40+
value = data.get(key, None)
41+
42+
if value:
43+
setattr(personalization, attr, value)
44+
getattr(personalization, attr)
45+
46+
return personalization

test/test_echo_to_stream.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import os
2-
import sys
31
import warnings
42
from unittest.mock import MagicMock
53

test/test_mail.py

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import base64
2-
import sys
32
from email.mime.image import MIMEImage
43

54
from django.core.mail import EmailMessage, EmailMultiAlternatives
65
from django.test import override_settings
76
from django.test.testcases import SimpleTestCase
7+
from sendgrid.helpers.mail import (
8+
CustomArg,
9+
Email,
10+
Header,
11+
Personalization,
12+
Substitution,
13+
)
814

915
from sendgrid_backend.mail import SendgridBackend
10-
from sendgrid_backend.util import SENDGRID_5
16+
from sendgrid_backend.util import SENDGRID_5, SENDGRID_6, dict_to_personalization
17+
18+
if SENDGRID_6:
19+
from sendgrid.helpers.mail import Bcc, Cc, To
1120

1221

1322
class TestMailGeneration(SimpleTestCase):
@@ -480,12 +489,109 @@ def test_EmailMessage_custom_args(self):
480489

481490
self.assertDictEqual(result, expected)
482491

483-
"""
484-
todo: Implement these
485-
def test_attachments(self):
486-
pass
492+
def test_personalizations_resolution(self):
493+
"""
494+
Tests that adding a Personalization() object directly to an EmailMessage object
495+
works as expected.
496+
497+
Written to test functionality introduced in the PR:
498+
https://github.com/sklarsa/django-sendgrid-v5/pull/90
499+
"""
500+
msg = EmailMessage(
501+
subject="Hello, World!",
502+
body="Hello, World!",
503+
from_email="Sam Smith <sam.smith@example.com>",
504+
to=["John Doe <john.doe@example.com>", "jane.doe@example.com"],
505+
cc=["Stephanie Smith <stephanie.smith@example.com>"],
506+
bcc=["Sarah Smith <sarah.smith@example.com>"],
507+
reply_to=["Sam Smith <sam.smith@example.com>"],
508+
)
509+
510+
# Tests that personalizations take priority
511+
test_str = "admin@my-test-domain.com"
512+
test_key_str = "my key"
513+
test_val_str = "my val"
514+
personalization = Personalization()
515+
516+
if SENDGRID_5:
517+
personalization.add_to(Email(test_str))
518+
personalization.add_cc(Email(test_str))
519+
personalization.add_bcc(Email(test_str))
520+
else:
521+
personalization.add_to(To(test_str))
522+
personalization.add_cc(Cc(test_str))
523+
personalization.add_bcc(Bcc(test_str))
524+
525+
personalization.add_custom_arg(CustomArg(test_key_str, test_val_str))
526+
personalization.add_header(Header(test_key_str, test_val_str))
527+
personalization.add_substitution(Substitution(test_key_str, test_val_str))
528+
529+
msg.personalizations = [personalization]
530+
531+
result = self.backend._build_sg_mail(msg)
532+
533+
personalization = result["personalizations"][0]
534+
535+
for field in ("to", "cc", "bcc"):
536+
data = personalization[field]
537+
self.assertEquals(len(data), 1)
538+
self.assertEquals(data[0]["email"], test_str)
539+
540+
for field in ("custom_args", "headers", "substitutions"):
541+
data = personalization[field]
542+
self.assertEquals(len(data), 1)
543+
self.assertIn(test_key_str, data)
544+
self.assertEquals(test_val_str, data[test_key_str])
545+
546+
def test_dict_to_personalization(self):
547+
"""
548+
Tests that dict_to_personalization works
549+
"""
550+
data = {
551+
"to": [
552+
{"email": "john.doe@example.com", "name": "John Doe"},
553+
{
554+
"email": "jane.doe@example.com",
555+
},
556+
],
557+
"cc": [
558+
{
559+
"email": "stephanie.smith@example.com",
560+
"name": "Stephanie Smith",
561+
}
562+
],
563+
"bcc": [{"email": "sarah.smith@example.com", "name": "Sarah Smith"}],
564+
"subject": "Hello, World!",
565+
"custom_args": {"arg_1": "Foo", "arg_2": "bar"},
566+
"headers": {"header_1": "Foo", "header_2": "Bar"},
567+
"substitutions": {"sub_a": "foo", "sub_b": "bar"},
568+
"send_at": 1518108670,
569+
"dynamic_template_data": {
570+
"subject": "Hello, World!",
571+
"content": "Hello, World!",
572+
"link": "http://hello.com",
573+
},
574+
}
487575

488-
def test_headers(self):
489-
pass
576+
p = dict_to_personalization(data)
577+
578+
fields_to_test = (
579+
("tos", "to"),
580+
("ccs", "cc"),
581+
("bccs", "bcc"),
582+
("subject", "subject"),
583+
("custom_args", "custom_args"),
584+
("headers", "headers"),
585+
("substitutions", "substitutions"),
586+
("send_at", "send_at"),
587+
("dynamic_template_data", "dynamic_template_data"),
588+
)
490589

491-
"""
590+
for arg, key in fields_to_test:
591+
val = getattr(p, arg)
592+
if type(val) == list:
593+
self.assertListEqual(val, data[key])
594+
elif type(val) == dict:
595+
self.assertDictEqual(val, data[key])
596+
else:
597+
self.assertEquals(val, data[key])

0 commit comments

Comments
 (0)