-
Notifications
You must be signed in to change notification settings - Fork 133
/
Copy pathbrevo.py
228 lines (189 loc) · 8.31 KB
/
brevo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
from requests.structures import CaseInsensitiveDict
from ..exceptions import AnymailRequestsAPIError
from ..message import AnymailRecipientStatus
from ..utils import BASIC_NUMERIC_TYPES, get_anymail_setting
from .base_requests import AnymailRequestsBackend, RequestsPayload
class EmailBackend(AnymailRequestsBackend):
"""
Brevo v3 API Email Backend
"""
esp_name = "Brevo"
def __init__(self, **kwargs):
"""Init options from Django settings"""
esp_name = self.esp_name
self.api_key = get_anymail_setting(
"api_key",
esp_name=esp_name,
kwargs=kwargs,
allow_bare=True,
)
api_url = get_anymail_setting(
"api_url",
esp_name=esp_name,
kwargs=kwargs,
default="https://api.brevo.com/v3/",
)
if not api_url.endswith("/"):
api_url += "/"
super().__init__(api_url, **kwargs)
def build_message_payload(self, message, defaults):
return BrevoPayload(message, defaults, self)
def parse_recipient_status(self, response, payload, message):
# Brevo doesn't give any detail on a success, other than messageId
# https://developers.brevo.com/reference/sendtransacemail
message_id = None
message_ids = []
if response.content != b"":
parsed_response = self.deserialize_json_response(response, payload, message)
try:
message_id = parsed_response["messageId"]
except (KeyError, TypeError):
try:
# batch send
message_ids = parsed_response["messageIds"]
except (KeyError, TypeError) as err:
raise AnymailRequestsAPIError(
"Invalid Brevo API response format",
email_message=message,
payload=payload,
response=response,
backend=self,
) from err
status = AnymailRecipientStatus(message_id=message_id, status="queued")
recipient_status = {
recipient.addr_spec: status for recipient in payload.all_recipients
}
if message_ids:
for to, message_id in zip(payload.to_recipients, message_ids):
recipient_status[to.addr_spec] = AnymailRecipientStatus(
message_id=message_id, status="queued"
)
return recipient_status
class BrevoPayload(RequestsPayload):
def __init__(self, message, defaults, backend, *args, **kwargs):
self.all_recipients = [] # used for backend.parse_recipient_status
self.to_recipients = [] # used for backend.parse_recipient_status
http_headers = kwargs.pop("headers", {})
http_headers["api-key"] = backend.api_key
http_headers["Content-Type"] = "application/json"
super().__init__(
message, defaults, backend, headers=http_headers, *args, **kwargs
)
def get_api_endpoint(self):
return "smtp/email"
def init_payload(self):
self.data = {"headers": CaseInsensitiveDict()} # becomes json
self.merge_data = {}
self.metadata = {}
self.merge_metadata = {}
self.merge_headers = {}
def serialize_data(self):
"""Performs any necessary serialization on self.data, and returns the result."""
if self.is_batch():
# Burst data["to"] into data["messageVersions"]
to_list = self.data.pop("to", [])
self.data["messageVersions"] = []
for to in to_list:
to_email = to["email"]
version = {"to": [to]}
headers = CaseInsensitiveDict()
if to_email in self.merge_data:
version["params"] = self.merge_data[to_email]
if to_email in self.merge_metadata:
# Merge global metadata with any per-recipient metadata.
# (Top-level X-Mailin-custom header already has global metadata,
# and will apply for recipients without version headers.)
recipient_metadata = self.metadata.copy()
recipient_metadata.update(self.merge_metadata[to_email])
headers["X-Mailin-custom"] = self.serialize_json(recipient_metadata)
if to_email in self.merge_headers:
headers.update(self.merge_headers[to_email])
if headers:
version["headers"] = headers
self.data["messageVersions"].append(version)
if not self.data["headers"]:
del self.data["headers"] # don't send empty headers
return self.serialize_json(self.data)
#
# Payload construction
#
@staticmethod
def email_object(email):
"""Converts EmailAddress to Brevo API array"""
email_object = dict()
email_object["email"] = email.addr_spec
if email.display_name:
email_object["name"] = email.display_name
return email_object
def set_from_email(self, email):
self.data["sender"] = self.email_object(email)
def set_recipients(self, recipient_type, emails):
assert recipient_type in ["to", "cc", "bcc"]
if emails:
self.data[recipient_type] = [self.email_object(email) for email in emails]
self.all_recipients += emails # used for backend.parse_recipient_status
if recipient_type == "to":
self.to_recipients = emails # used for backend.parse_recipient_status
def set_subject(self, subject):
if subject != "": # see note in set_text_body about template rendering
self.data["subject"] = subject
def set_reply_to(self, emails):
# Brevo only supports a single address in the reply_to API param.
if len(emails) > 1:
self.unsupported_feature("multiple reply_to addresses")
if len(emails) > 0:
self.data["replyTo"] = self.email_object(emails[0])
def set_extra_headers(self, headers):
# Brevo requires header values to be strings (not integers) as of 11/2022.
# Stringify ints and floats; anything else is the caller's responsibility.
self.data["headers"].update(
{
k: str(v) if isinstance(v, BASIC_NUMERIC_TYPES) else v
for k, v in headers.items()
}
)
def set_tags(self, tags):
if len(tags) > 0:
self.data["tags"] = tags
def set_template_id(self, template_id):
self.data["templateId"] = template_id
def set_text_body(self, body):
if body:
self.data["textContent"] = body
def set_html_body(self, body):
if body:
if "htmlContent" in self.data:
self.unsupported_feature("multiple html parts")
self.data["htmlContent"] = body
def add_attachment(self, attachment):
"""Converts attachments to Brevo API {name, base64} array"""
att = {
"name": attachment.name or "",
"content": attachment.b64content,
}
if attachment.inline:
self.unsupported_feature("inline attachments")
self.data.setdefault("attachment", []).append(att)
def set_esp_extra(self, extra):
self.data.update(extra)
def set_merge_data(self, merge_data):
# Late bound in serialize_data:
self.merge_data = merge_data
def set_merge_global_data(self, merge_global_data):
self.data["params"] = merge_global_data
def set_metadata(self, metadata):
# Brevo expects a single string payload
self.data["headers"]["X-Mailin-custom"] = self.serialize_json(metadata)
self.metadata = metadata # needed in serialize_data for batch send
def set_merge_metadata(self, merge_metadata):
# Late-bound in serialize_data:
self.merge_metadata = merge_metadata
def set_merge_headers(self, merge_headers):
# Late-bound in serialize_data:
self.merge_headers = merge_headers
def set_send_at(self, send_at):
try:
start_time_iso = send_at.isoformat(timespec="milliseconds")
except (AttributeError, TypeError):
start_time_iso = send_at # assume user already formatted
self.data["scheduledAt"] = start_time_iso