1+ import warnings
2+
13from django .core .mail import make_msgid
24from requests .structures import CaseInsensitiveDict
35
4- from ..exceptions import AnymailConfigurationError , AnymailRequestsAPIError
6+ from ..exceptions import AnymailConfigurationError , AnymailRequestsAPIError , AnymailWarning
57from ..message import AnymailRecipientStatus
68from ..utils import get_anymail_setting , timestamp
79
@@ -31,6 +33,8 @@ def __init__(self, **kwargs):
3133
3234 self .generate_message_id = get_anymail_setting ('generate_message_id' , esp_name = esp_name ,
3335 kwargs = kwargs , default = True )
36+ self .merge_field_format = get_anymail_setting ('merge_field_format' , esp_name = esp_name ,
37+ kwargs = kwargs , default = None )
3438
3539 # This is SendGrid's Web API v2 (because the Web API v3 doesn't support sending)
3640 api_url = get_anymail_setting ('api_url' , esp_name = esp_name , kwargs = kwargs ,
@@ -65,6 +69,10 @@ def __init__(self, message, defaults, backend, *args, **kwargs):
6569 self .generate_message_id = backend .generate_message_id
6670 self .message_id = None # Message-ID -- assigned in serialize_data unless provided in headers
6771 self .smtpapi = {} # SendGrid x-smtpapi field
72+ self .to_list = [] # late-bound 'to' field
73+ self .merge_field_format = backend .merge_field_format
74+ self .merge_data = None # late-bound per-recipient data
75+ self .merge_global_data = None
6876
6977 http_headers = kwargs .pop ('headers' , {})
7078 query_params = kwargs .pop ('params' , {})
@@ -86,6 +94,15 @@ def serialize_data(self):
8694 if self .generate_message_id :
8795 self .ensure_message_id ()
8896
97+ self .build_merge_data ()
98+ if self .merge_data is None :
99+ # Standard 'to' and 'toname' headers
100+ self .set_recipients ('to' , self .to_list )
101+ else :
102+ # Merge-friendly smtpapi 'to' field
103+ self .smtpapi ['to' ] = [email .address for email in self .to_list ]
104+ self .all_recipients += self .to_list
105+
89106 # Serialize x-smtpapi to json:
90107 if len (self .smtpapi ) > 0 :
91108 # If esp_extra was also used to set x-smtpapi, need to merge it
@@ -132,6 +149,41 @@ def make_message_id(self):
132149 domain = None
133150 return make_msgid (domain = domain )
134151
152+ def build_merge_data (self ):
153+ """Set smtpapi['sub'] and ['section']"""
154+ if self .merge_data is not None :
155+ # Convert from {to1: {a: A1, b: B1}, to2: {a: A2}} (merge_data format)
156+ # to {a: [A1, A2], b: [B1, ""]} ({field: [data in to-list order], ...})
157+ all_fields = set ()
158+ for recipient_data in self .merge_data .values ():
159+ all_fields = all_fields .union (recipient_data .keys ())
160+ recipients = [email .email for email in self .to_list ]
161+
162+ if self .merge_field_format is None and all (field .isalnum () for field in all_fields ):
163+ warnings .warn (
164+ "Your SendGrid merge fields don't seem to have delimiters, "
165+ "which can cause unexpected results with Anymail's merge_data. "
166+ "Search SENDGRID_MERGE_FIELD_FORMAT in the Anymail docs for more info." ,
167+ AnymailWarning )
168+
169+ sub_field_fmt = self .merge_field_format or '{}'
170+ sub_fields = {field : sub_field_fmt .format (field ) for field in all_fields }
171+
172+ self .smtpapi ['sub' ] = {
173+ # If field data is missing for recipient, use (formatted) field as the substitution.
174+ # (This allows default to resolve from global "section" substitutions.)
175+ sub_fields [field ]: [self .merge_data .get (recipient , {}).get (field , sub_fields [field ])
176+ for recipient in recipients ]
177+ for field in all_fields
178+ }
179+
180+ if self .merge_global_data is not None :
181+ section_field_fmt = self .merge_field_format or '{}'
182+ self .smtpapi ['section' ] = {
183+ section_field_fmt .format (field ): data
184+ for field , data in self .merge_global_data .items ()
185+ }
186+
135187 #
136188 # Payload construction
137189 #
@@ -146,6 +198,11 @@ def set_from_email(self, email):
146198 if email .name :
147199 self .data ["fromname" ] = email .name
148200
201+ def set_to (self , emails ):
202+ # late-bind in self.serialize_data, because whether it goes in smtpapi
203+ # depends on whether there is merge_data
204+ self .to_list = emails
205+
149206 def set_recipients (self , recipient_type , emails ):
150207 assert recipient_type in ["to" , "cc" , "bcc" ]
151208 if emails :
@@ -229,5 +286,18 @@ def set_track_opens(self, track_opens):
229286 # (You could add it through esp_extra.)
230287 self .add_filter ('opentrack' , 'enable' , int (track_opens ))
231288
289+ def set_template_id (self , template_id ):
290+ self .add_filter ('templates' , 'enable' , 1 )
291+ self .add_filter ('templates' , 'template_id' , template_id )
292+
293+ def set_merge_data (self , merge_data ):
294+ # Becomes smtpapi['sub'] in build_merge_data, after we know recipients and merge_field_format.
295+ self .merge_data = merge_data
296+
297+ def set_merge_global_data (self , merge_global_data ):
298+ # Becomes smtpapi['section'] in build_merge_data, after we know merge_field_format.
299+ self .merge_global_data = merge_global_data
300+
232301 def set_esp_extra (self , extra ):
302+ self .merge_field_format = extra .pop ('merge_field_format' , self .merge_field_format )
233303 self .data .update (extra )
0 commit comments