sendgrid / sendgrid-python

The Official Twilio SendGrid Python API Library
https://sendgrid.com
MIT License
1.53k stars 711 forks source link

is_multiple=True prevents use of dynamic template data #961

Open ghost opened 3 years ago

ghost commented 3 years ago

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

  1. Create a simple Mail object, like so: Mail(from_email='someone@something.com', to_emails=['one', 'two', ...], is_multiple=True
  2. Set the dynamic_template_data property.
  3. Set the template_id property.
  4. 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='someone@something.com',
        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:

JenniferMah commented 3 years ago

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.

ksho commented 3 years ago

+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.

ghost commented 3 years ago

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.

ksho commented 3 years ago

..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='foo@bar.com',
            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)
ghost commented 3 years ago

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': 'example@test.com'}
        {'name': 'example1', 'email': 'example1@test.com'}
        {'name': 'example2', 'email': 'example2@test.com'}
    ]
    dynamic_template_data = {
        'subject': 'This will be individually copied to each message.'
    }

    message = Mail(from_email='myemail@me.com')
    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': 'example@test.com'}
        {'name': 'example1', 'email': 'example1@test.com'}
        {'name': 'example2', 'email': 'example2@test.com'}
    ]
    dynamic_template_data = {
        'subject': 'This will be individually copied to each message.'
    }

    message = Mail.create_single_template_email_with_multiple_recipients(
        from_email='myemail@me.com', 
        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.

keatbro commented 3 years ago

+1 on this—eating up API requests trying to workaround

prayashm commented 3 years ago

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.