is_multiple=True prevents use of dynamic template data
Issue Summary
Hey all,
Using the mail helper with is_multiple=True causes the dynamic_template_data on the message not to propagate to the actual message. It seems that this data has to somehow be included individually in each personalization object, but I don't think I can set the Mail object's personalizations property manually, meaning there's no way to push the dynamic_template_data down into each of those objects.
If this is a bug, I would suggest automatically copying the dynamic_template_data to each personalization object when is_multiple is set to True.
Steps to Reproduce
- Create a simple Mail object, like so:
Mail(from_email='[email protected]', to_emails=['one', 'two', ...], is_multiple=True - Set the
dynamic_template_dataproperty. - Set the
template_idproperty. - Send the email. Notice that all recipients receive it as they should, but that the dynamic template data does not come along for the ride.
Code Snippet
def send_notification_email(emails: list, dynamic_data: dict, file_content: str):
message = Mail(
from_email='[email protected]',
to_emails=[To(email=email) for email in emails],
is_multiple=True)
message.dynamic_template_data = dynamic_data
message.template_id = TemplateId('atemplateid')
message.attachment = Attachment(FileContent(file_content), FileName('somename.someextension'))
message.asm = Asm(GroupId(12345), GroupsToDisplay([12345]))
sendgrid_client = SendGridAPIClient(os.environ['SENDGRID_API_KEY'])
response = sendgrid_client.send(message)
if response.status_code != 202:
logging.info(response.status_code)
raise Exception("Email send failed.")
return response.body
Exception/Log
N/A, code executes "successfully".
Technical details:
- sendgrid-python version: 6.4.8
- python version: 3.8.6 64bit
This issue has been added to our internal backlog to be prioritized. Pull requests and +1s on the issue summary will help it move up the backlog.
+1 on this -- the workaround I have implemented as a stopgap is looping through all emails to send to a single address at a time. Not great for latency, but works for now.
Just an FYI to anyone considering picking this up: The C# library already has this feature implemented as SendGrid.Helpers.Mail.MailHelper.CreateSingleTemplateEmailToMultipleRecipients. Effectively, it just internalizes what @ksho mentioned above -- if showAllRecipients is false (default), it creates a personalization for each recipient and copies the template data into it. This pattern might be useful for implementation in Python.
..and just to clarify a bit further, that workaround is something like:
members = list of email strings
for email in members:
message = Mail(
from_email='[email protected]',
to_emails=email,
subject='lalala')
message.template_id = TemplateId('d-123456')
request_body = message.get()
request_body['personalizations'][0]['dynamic_template_data'] = substitution_data
try:
r = sg.client.mail.send.post(request_body=request_body)
I have a pretty good idea of how this could be implemented...
Here's the way the code should work without a builtin helper from sendgrid.helpers.mail:
import sendgrid
from sendgrid.helpers.mail import Mail
if __name__ == "__main__":
# We don't want these emails to be able to see each other in the "To" bar, but they should all receive the same email.
tos = [
{'name': 'example', 'email': '[email protected]'}
{'name': 'example1', 'email': '[email protected]'}
{'name': 'example2', 'email': '[email protected]'}
]
dynamic_template_data = {
'subject': 'This will be individually copied to each message.'
}
message = Mail(from_email='[email protected]')
message.template_id = "something"
# This is how we'd do it in C#, and this pattern is embedded into helper methods on MailHelper
# the MailHelper class' helper methods in C# could easily be modelled as class methods on the Mail object in Python
for i, to in enumerate(tos):
message.add_to(to['email'], p=i)
# this is what needs to be implemented and then incorporated into a constructor/class method on Mail
# to be abundently clear, Mail.set_template_data does not currently exist
message.set_template_data(dynamic_template_data, p=i)
client = sendgrid.SendGridAPIClient(key)
client.send(message)
And now with a class method helper:
import sendgrid
from sendgrid.helpers.mail import Mail
if __name__ == "__main__":
# We don't want these emails to be able to see each other in the "To" bar, but they should all receive the same email.
tos = [
{'name': 'example', 'email': '[email protected]'}
{'name': 'example1', 'email': '[email protected]'}
{'name': 'example2', 'email': '[email protected]'}
]
dynamic_template_data = {
'subject': 'This will be individually copied to each message.'
}
message = Mail.create_single_template_email_with_multiple_recipients(
from_email='[email protected]',
tos=tos, # I don't remember if the object format above is appropriate for the Python library, but this at least proves the pattern
plain_text_content="hi!",
html_content="<p>hi!</p>"
dynamic_template_data=dynamic_template_data)
client = sendgrid.SendGridAPIClient(key)
client.send(message)
To achieve this, we'd need five class methods on Mail:
# Pretend there's a subject/plaintext/html argument for each of the following -- I don't want to write them out
@classmethod
def create_single_email(cls, from_email: str, to_email: str, template_id: str, dynamic_template_data: object) {
#return an instance of the class with the required personalization
}
@classmethod
def create_single_email_to_multiple_recipients(cls, from_email: str, to_emails: list<str>) {
#return an instance of the class with the required personalizations (one for each to)
#super easy -- instantiate the class, then just iterate over enumerate(to_emails), calling
#classobject.add_to(email, i)
}
@classmethod
def create_single_template_email_to_multiple_recipients(yougettheidea) {
#Same as the above, but call classobject.add_to(email, i) and then classobject.set_template_data(dynamic_template_data, i) (not implemented)
}
#Two more class methods with similar logic for create_multiple_emails_to_multiple_recipients and create_multiple_template_emails_to_multiple_recipients
I think the only other methods we'd have to implement are (instance methods on Mail):
def set_template_data(self, dynamic_template_data: object, p: int):
personalization = self.GetPersonalization(p)
personalization.TemplateData = dynamic_template_data
def get_personalization(self, p: int):
# return self's personalization at the given index, assuming it exists. Throw index errors if we're being messy.
# if there's no personalization, create one at index 0 and return it
I'm sure there are some implementation details that might make this slightly more difficult, but those are the bones we'd need. If I wasn't so busy, I'd try to tackle it myself.
+1 on this—eating up API requests trying to workaround
Works in sendgrid==6.6.0 and python 3.7.9 as documentated here:
https://github.com/sendgrid/sendgrid-python/blob/main/use_cases/send_multiple_emails_to_multiple_recipients.md
dynamic_template_data needs to be passed in To() constructor.