python-missive 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymissive/__init__.py +3 -0
- pymissive/__main__.py +9 -0
- pymissive/archives/__init__.py +142 -0
- pymissive/archives/__main__.py +9 -0
- pymissive/archives/address.py +272 -0
- pymissive/archives/address_backends/__init__.py +29 -0
- pymissive/archives/address_backends/base.py +610 -0
- pymissive/archives/address_backends/geoapify.py +221 -0
- pymissive/archives/address_backends/geocode_earth.py +210 -0
- pymissive/archives/address_backends/google_maps.py +371 -0
- pymissive/archives/address_backends/here.py +348 -0
- pymissive/archives/address_backends/locationiq.py +271 -0
- pymissive/archives/address_backends/mapbox.py +314 -0
- pymissive/archives/address_backends/maps_co.py +257 -0
- pymissive/archives/address_backends/nominatim.py +348 -0
- pymissive/archives/address_backends/opencage.py +292 -0
- pymissive/archives/address_backends/pelias_mixin.py +181 -0
- pymissive/archives/address_backends/photon.py +322 -0
- pymissive/archives/cli.py +42 -0
- pymissive/archives/helpers.py +45 -0
- pymissive/archives/missive.py +64 -0
- pymissive/archives/providers/__init__.py +167 -0
- pymissive/archives/providers/apn.py +171 -0
- pymissive/archives/providers/ar24.py +204 -0
- pymissive/archives/providers/base/__init__.py +203 -0
- pymissive/archives/providers/base/_attachments.py +166 -0
- pymissive/archives/providers/base/branded.py +341 -0
- pymissive/archives/providers/base/common.py +781 -0
- pymissive/archives/providers/base/email.py +422 -0
- pymissive/archives/providers/base/email_message.py +85 -0
- pymissive/archives/providers/base/monitoring.py +150 -0
- pymissive/archives/providers/base/notification.py +187 -0
- pymissive/archives/providers/base/postal.py +742 -0
- pymissive/archives/providers/base/postal_defaults.py +26 -0
- pymissive/archives/providers/base/sms.py +213 -0
- pymissive/archives/providers/base/voice_call.py +82 -0
- pymissive/archives/providers/brevo.py +363 -0
- pymissive/archives/providers/certeurope.py +249 -0
- pymissive/archives/providers/django_email.py +182 -0
- pymissive/archives/providers/fcm.py +91 -0
- pymissive/archives/providers/laposte.py +392 -0
- pymissive/archives/providers/maileva.py +511 -0
- pymissive/archives/providers/mailgun.py +118 -0
- pymissive/archives/providers/messenger.py +74 -0
- pymissive/archives/providers/notification.py +112 -0
- pymissive/archives/providers/sendgrid.py +160 -0
- pymissive/archives/providers/ses.py +185 -0
- pymissive/archives/providers/signal.py +68 -0
- pymissive/archives/providers/slack.py +80 -0
- pymissive/archives/providers/smtp.py +190 -0
- pymissive/archives/providers/teams.py +91 -0
- pymissive/archives/providers/telegram.py +69 -0
- pymissive/archives/providers/twilio.py +310 -0
- pymissive/archives/providers/vonage.py +208 -0
- pymissive/archives/sender.py +339 -0
- pymissive/archives/status.py +22 -0
- pymissive/cli.py +42 -0
- pymissive/config.py +397 -0
- pymissive/helpers.py +0 -0
- pymissive/providers/apn.py +8 -0
- pymissive/providers/ar24.py +8 -0
- pymissive/providers/base/__init__.py +64 -0
- pymissive/providers/base/acknowledgement.py +6 -0
- pymissive/providers/base/attachments.py +10 -0
- pymissive/providers/base/branded.py +16 -0
- pymissive/providers/base/email.py +2 -0
- pymissive/providers/base/notification.py +2 -0
- pymissive/providers/base/postal.py +2 -0
- pymissive/providers/base/sms.py +2 -0
- pymissive/providers/base/voice_call.py +2 -0
- pymissive/providers/brevo.py +420 -0
- pymissive/providers/certeurope.py +8 -0
- pymissive/providers/django_email.py +8 -0
- pymissive/providers/fcm.py +8 -0
- pymissive/providers/laposte.py +8 -0
- pymissive/providers/maileva.py +8 -0
- pymissive/providers/mailgun.py +8 -0
- pymissive/providers/messenger.py +8 -0
- pymissive/providers/notification.py +8 -0
- pymissive/providers/partner.py +650 -0
- pymissive/providers/scaleway.py +498 -0
- pymissive/providers/sendgrid.py +8 -0
- pymissive/providers/ses.py +8 -0
- pymissive/providers/signal.py +8 -0
- pymissive/providers/slack.py +8 -0
- pymissive/providers/smtp.py +8 -0
- pymissive/providers/teams.py +8 -0
- pymissive/providers/telegram.py +8 -0
- pymissive/providers/twilio.py +8 -0
- pymissive/providers/vonage.py +8 -0
- python_missive-0.2.0.dist-info/METADATA +152 -0
- python_missive-0.2.0.dist-info/RECORD +95 -0
- python_missive-0.2.0.dist-info/WHEEL +5 -0
- python_missive-0.2.0.dist-info/entry_points.txt +2 -0
- python_missive-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Brevo (ex Sendinblue) email provider - API and SMTP modes."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
from .base import MissiveProviderBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BrevoAPIProvider(MissiveProviderBase):
|
|
10
|
+
"""Abstract base class for Brevo providers."""
|
|
11
|
+
|
|
12
|
+
# -----------------------
|
|
13
|
+
# Metadata / configuration
|
|
14
|
+
# -----------------------
|
|
15
|
+
name = "brevo"
|
|
16
|
+
display_name = "Brevo"
|
|
17
|
+
required_packages = ["brevo-python"]
|
|
18
|
+
config_keys = ["EMAIL_API_KEY", "SMS_API_KEY", "WHATSAPP_API_KEY"]
|
|
19
|
+
description = "Complete CRM platform (Email, SMS, Marketing automation)"
|
|
20
|
+
documentation_url = "https://developers.brevo.com"
|
|
21
|
+
site_url = "https://www.brevo.com"
|
|
22
|
+
brands = ["WhatsApp"]
|
|
23
|
+
fields_associations = {
|
|
24
|
+
"id": "id",
|
|
25
|
+
"url": "url",
|
|
26
|
+
"type": "type",
|
|
27
|
+
"description": "description",
|
|
28
|
+
"created_at": "createdAt",
|
|
29
|
+
"updated_at": "updatedAt",
|
|
30
|
+
}
|
|
31
|
+
events_association = {
|
|
32
|
+
"request": "request",
|
|
33
|
+
"sent": "sent",
|
|
34
|
+
"hardBounce": "hard_bounce",
|
|
35
|
+
"softBounce": "soft_bounce",
|
|
36
|
+
"blocked": "blocked",
|
|
37
|
+
"spam": "spam",
|
|
38
|
+
"delivered": "delivered",
|
|
39
|
+
"click": "click",
|
|
40
|
+
"invalid": "invalid",
|
|
41
|
+
"deferred": "deferred",
|
|
42
|
+
"opened": "opened",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#########################################################
|
|
46
|
+
# Initialization / API clients
|
|
47
|
+
#########################################################
|
|
48
|
+
|
|
49
|
+
def __init__(self, **kwargs: str | None) -> None:
|
|
50
|
+
super().__init__(**kwargs)
|
|
51
|
+
if not hasattr(self, "attachments"):
|
|
52
|
+
self.attachments = []
|
|
53
|
+
self._email_api_key = self._get_config_or_env("EMAIL_API_KEY")
|
|
54
|
+
self._sms_api_key = self._get_config_or_env("SMS_API_KEY")
|
|
55
|
+
self._whatsapp_api_key = self._get_config_or_env("WHATSAPP_API_KEY")
|
|
56
|
+
self._webhooks_api = None
|
|
57
|
+
self._transactional_email_api = None
|
|
58
|
+
self._transactional_sms_api = None
|
|
59
|
+
self._transactional_whatsapp_api = None
|
|
60
|
+
|
|
61
|
+
def get_api_client(self, api_key: str):
|
|
62
|
+
"""Return the Brevo API client."""
|
|
63
|
+
from brevo_python import ApiClient, Configuration
|
|
64
|
+
configuration = Configuration()
|
|
65
|
+
configuration.api_key["api-key"] = api_key
|
|
66
|
+
configuration.api_key['partner-key'] = api_key
|
|
67
|
+
return ApiClient(configuration)
|
|
68
|
+
|
|
69
|
+
def _get_transactional_email_api(self):
|
|
70
|
+
"""Return the Brevo transactional API instance."""
|
|
71
|
+
if self._transactional_email_api is None:
|
|
72
|
+
from brevo_python import TransactionalEmailsApi
|
|
73
|
+
api_client = self.get_api_client(self._email_api_key)
|
|
74
|
+
self._transactional_email_api = TransactionalEmailsApi(api_client)
|
|
75
|
+
return self._transactional_email_api
|
|
76
|
+
|
|
77
|
+
#########################################################
|
|
78
|
+
# Webhooks
|
|
79
|
+
#########################################################
|
|
80
|
+
|
|
81
|
+
def _get_webhooks_api(self):
|
|
82
|
+
"""Return the Brevo webhooks API instance."""
|
|
83
|
+
if self._webhooks_api is None:
|
|
84
|
+
from brevo_python import WebhooksApi
|
|
85
|
+
api_client = self.get_api_client(self._email_api_key)
|
|
86
|
+
self._webhooks_api = WebhooksApi(api_client)
|
|
87
|
+
return self._webhooks_api
|
|
88
|
+
|
|
89
|
+
def get_webhooks(self):
|
|
90
|
+
"""Return the Brevo webhooks."""
|
|
91
|
+
wbhs = self._get_webhooks_api()
|
|
92
|
+
return wbhs.get_webhooks().webhooks
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
#########################################################
|
|
96
|
+
# Helpers
|
|
97
|
+
#########################################################
|
|
98
|
+
|
|
99
|
+
def get_external_id_email(self, payload: dict[str, Any]) -> str | None:
|
|
100
|
+
"""Extract the external ID from the Brevo webhook."""
|
|
101
|
+
return payload.get("_message_id")
|
|
102
|
+
|
|
103
|
+
def get_normalize_type(self, data: dict[str, Any]) -> str:
|
|
104
|
+
"""Return the normalized type of webhook/email/SMS."""
|
|
105
|
+
if data.get("type") == "transactional":
|
|
106
|
+
return "email"
|
|
107
|
+
elif data.get("type") == "marketing":
|
|
108
|
+
return "email_marketing"
|
|
109
|
+
elif data.get("type") == "sms":
|
|
110
|
+
return "sms"
|
|
111
|
+
return "unknown"
|
|
112
|
+
|
|
113
|
+
#########################################################
|
|
114
|
+
# Attachments
|
|
115
|
+
#########################################################
|
|
116
|
+
|
|
117
|
+
def _add_attachments_email(self, email, attachments):
|
|
118
|
+
if not attachments:
|
|
119
|
+
return
|
|
120
|
+
from brevo_python import SendSmtpEmailAttachment
|
|
121
|
+
|
|
122
|
+
email.attachment = [
|
|
123
|
+
SendSmtpEmailAttachment(
|
|
124
|
+
name=a["name"],
|
|
125
|
+
content=self._to_base64(a["content"]),
|
|
126
|
+
)
|
|
127
|
+
for a in attachments
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
#########################################################
|
|
131
|
+
# Email sending
|
|
132
|
+
#########################################################
|
|
133
|
+
|
|
134
|
+
def delete_blocked_emails(self, kwargs: dict[str, Any]) -> bool:
|
|
135
|
+
with contextlib.suppress(Exception):
|
|
136
|
+
api_instance = self._get_transactional_email_api()
|
|
137
|
+
for recipient in kwargs.get("recipients", []):
|
|
138
|
+
api_instance.smtp_blocked_contacts_email_delete(recipient["email"])
|
|
139
|
+
for recipient in kwargs.get("cc", []):
|
|
140
|
+
api_instance.smtp_blocked_contacts_email_delete(recipient["email"])
|
|
141
|
+
for recipient in kwargs.get("bcc", []):
|
|
142
|
+
api_instance.smtp_blocked_contacts_email_delete(recipient["email"])
|
|
143
|
+
|
|
144
|
+
def _prepare_email(self, **kwargs):
|
|
145
|
+
"""Prepare the SendSmtpEmail object for sending."""
|
|
146
|
+
from brevo_python import SendSmtpEmail
|
|
147
|
+
email = SendSmtpEmail(subject=kwargs["subject"])
|
|
148
|
+
self._add_sender(email, kwargs)
|
|
149
|
+
self._add_content(email, kwargs)
|
|
150
|
+
self._add_reply_to(email, kwargs)
|
|
151
|
+
self._add_recipients(email, kwargs["recipients"])
|
|
152
|
+
self._add_bcc_or_cc(email, kwargs.get("bcc", []), "bcc")
|
|
153
|
+
self._add_bcc_or_cc(email, kwargs.get("cc", []), "cc")
|
|
154
|
+
self._add_attachments_email(email, kwargs.get("attachments", []))
|
|
155
|
+
return email
|
|
156
|
+
|
|
157
|
+
def _add_recipients(self, email, recipients):
|
|
158
|
+
from brevo_python import SendSmtpEmailTo
|
|
159
|
+
email.to = [
|
|
160
|
+
SendSmtpEmailTo(email=recipient["email"], name=recipient.get("name", ""))
|
|
161
|
+
for recipient in recipients
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
def _add_sender(self, email, kwargs):
|
|
165
|
+
from brevo_python import SendSmtpEmailSender
|
|
166
|
+
sender = kwargs.get("sender")
|
|
167
|
+
email.sender = SendSmtpEmailSender(
|
|
168
|
+
email=sender["email"],
|
|
169
|
+
name=sender.get("name", "")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _add_content(self, email, kwargs):
|
|
173
|
+
if kwargs.get("body"):
|
|
174
|
+
email.html_content = kwargs["body"]
|
|
175
|
+
if kwargs.get("body_text"):
|
|
176
|
+
email.text_content = kwargs["body_text"]
|
|
177
|
+
|
|
178
|
+
def _add_reply_to(self, email, kwargs):
|
|
179
|
+
reply_to = kwargs.get("reply_to")
|
|
180
|
+
if reply_to:
|
|
181
|
+
from brevo_python import SendSmtpEmailReplyTo
|
|
182
|
+
email.reply_to = SendSmtpEmailReplyTo(
|
|
183
|
+
email=reply_to["email"],
|
|
184
|
+
name=reply_to.get("name", "")
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _add_bcc_or_cc(self, email, recipients, key):
|
|
188
|
+
if not recipients or key not in ["cc", "bcc"]:
|
|
189
|
+
return
|
|
190
|
+
if key == "cc":
|
|
191
|
+
from brevo_python import SendSmtpEmailCc
|
|
192
|
+
recipient_class = SendSmtpEmailCc
|
|
193
|
+
elif key == "bcc":
|
|
194
|
+
from brevo_python import SendSmtpEmailBcc
|
|
195
|
+
recipient_class = SendSmtpEmailBcc
|
|
196
|
+
setattr(email, key, [
|
|
197
|
+
recipient_class(email=r["email"], name=r.get("name", ""))
|
|
198
|
+
for r in recipients
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
def send_email(self, **kwargs) -> dict[str, Any]:
|
|
202
|
+
"""Send email via Brevo API."""
|
|
203
|
+
self.delete_blocked_emails(kwargs)
|
|
204
|
+
email = self._prepare_email(**kwargs)
|
|
205
|
+
api_instance = self._get_transactional_email_api()
|
|
206
|
+
response = api_instance.send_transac_email(email)
|
|
207
|
+
return {field: str(getattr(response, field)) for field in response.__dict__}
|
|
208
|
+
|
|
209
|
+
def set_webhook_email(self, webhook_data: dict[str, Any]) -> bool:
|
|
210
|
+
"""Configure a webhook to receive Brevo events."""
|
|
211
|
+
from brevo_python import CreateWebhook
|
|
212
|
+
wbhs = self._get_webhooks_api()
|
|
213
|
+
create_webhook = CreateWebhook(
|
|
214
|
+
url=webhook_data.get("url"),
|
|
215
|
+
description="Missive webhook email",
|
|
216
|
+
events=list(self.events_association.keys()),
|
|
217
|
+
channel="email",
|
|
218
|
+
type="transactional",
|
|
219
|
+
)
|
|
220
|
+
wbh = wbhs.create_webhook(create_webhook)
|
|
221
|
+
return self.get_normalize_webhook_id({"id": wbh.id})
|
|
222
|
+
|
|
223
|
+
def delete_webhook_email(self, webhook_data: dict[str, Any]) -> bool:
|
|
224
|
+
"""Delete a webhook from Brevo."""
|
|
225
|
+
wbhs = self._get_webhooks_api()
|
|
226
|
+
return wbhs.delete_webhook(webhook_data.get("id"))
|
|
227
|
+
|
|
228
|
+
def update_webhook_email(self, webhook_data: dict[str, Any]) -> bool:
|
|
229
|
+
"""Return the Brevo webhooks."""
|
|
230
|
+
from brevo_python import UpdateWebhook
|
|
231
|
+
wbhs = self._get_webhooks_api()
|
|
232
|
+
update = UpdateWebhook(url=webhook_data.get("url"))
|
|
233
|
+
webhook_id = webhook_data.get("id")
|
|
234
|
+
wbhs.update_webhook(webhook_id, update)
|
|
235
|
+
return self.get_normalize_webhook_id({"id": webhook_id})
|
|
236
|
+
|
|
237
|
+
def get_webhook_email(self, webhook_id: str):
|
|
238
|
+
"""Return the Brevo webhook."""
|
|
239
|
+
wbhs = self._get_webhooks_api()
|
|
240
|
+
return next((wbh for wbh in wbhs if str(wbh.id) == str(webhook_id)), None)
|
|
241
|
+
|
|
242
|
+
def get_webhooks_email(self):
|
|
243
|
+
"""Return only transactional email webhooks."""
|
|
244
|
+
webhooks = self.get_webhooks()
|
|
245
|
+
return [webhook for webhook in webhooks if webhook["type"] == "transactional"]
|
|
246
|
+
|
|
247
|
+
def handle_webhook_email(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
248
|
+
"""Handle a Brevo webhook and normalize event type."""
|
|
249
|
+
payload = payload.decode("utf-8")
|
|
250
|
+
payload = json.loads(payload)
|
|
251
|
+
event = payload.get("event")
|
|
252
|
+
message_id = payload.get("message-id") or payload.get("messageId")
|
|
253
|
+
return {
|
|
254
|
+
"recipient": payload.get("email"),
|
|
255
|
+
"external_id": str(message_id),
|
|
256
|
+
"event": self.events_association.get(event, "unknown"),
|
|
257
|
+
"description": payload.get("reason"),
|
|
258
|
+
"occurred_at": payload.get("date"),
|
|
259
|
+
"trace": payload,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
#########################################################
|
|
264
|
+
# SMS
|
|
265
|
+
#########################################################
|
|
266
|
+
|
|
267
|
+
def _get_transactional_sms_api(self):
|
|
268
|
+
"""Return the Brevo transactional SMS API instance."""
|
|
269
|
+
if self._transactional_sms_api is None:
|
|
270
|
+
from brevo_python import TransactionalSMSApi
|
|
271
|
+
api_client = self.get_api_client(self._sms_api_key)
|
|
272
|
+
self._transactional_sms_api = TransactionalSMSApi(api_client)
|
|
273
|
+
return self._transactional_sms_api
|
|
274
|
+
|
|
275
|
+
def _prepare_sms(self, **kwargs):
|
|
276
|
+
"""Prepare the SendSmsEmail object for sending."""
|
|
277
|
+
from brevo_python import SendTransacSms
|
|
278
|
+
sms = SendTransacSms(
|
|
279
|
+
sender=kwargs["sender"].get("name"),
|
|
280
|
+
recipient=str(kwargs["recipients"][0].get("phone")),
|
|
281
|
+
content=kwargs.get("body_text"),
|
|
282
|
+
)
|
|
283
|
+
return sms
|
|
284
|
+
|
|
285
|
+
def send_sms(self, **kwargs) -> dict[str, Any]:
|
|
286
|
+
"""Send SMS via Brevo API."""
|
|
287
|
+
sms = self._prepare_sms(**kwargs)
|
|
288
|
+
api_instance = self._get_transactional_sms_api()
|
|
289
|
+
response = api_instance.send_transac_sms(sms)
|
|
290
|
+
return {field: str(getattr(response, field)) for field in response.__dict__}
|
|
291
|
+
|
|
292
|
+
def set_webhook_sms(self, webhook_data: dict[str, Any]) -> bool:
|
|
293
|
+
"""Configure a webhook to receive Brevo events."""
|
|
294
|
+
from brevo_python import CreateWebhook
|
|
295
|
+
wbhs = self._get_webhooks_api()
|
|
296
|
+
create_webhook = CreateWebhook(
|
|
297
|
+
url=webhook_data.get("url"),
|
|
298
|
+
description="Missive webhook SMS",
|
|
299
|
+
events=list(self.events_association.keys()),
|
|
300
|
+
channel="sms",
|
|
301
|
+
type="transactional",
|
|
302
|
+
)
|
|
303
|
+
return wbhs.create_webhook(create_webhook)
|
|
304
|
+
|
|
305
|
+
def delete_webhook_sms(self, webhook_data: dict[str, Any]) -> bool:
|
|
306
|
+
"""Delete a webhook from Brevo."""
|
|
307
|
+
wbhs = self._get_webhooks_api()
|
|
308
|
+
return wbhs.delete_webhook(webhook_data.get("id"))
|
|
309
|
+
|
|
310
|
+
def update_webhook_sms(self, webhook_data: dict[str, Any]) -> bool:
|
|
311
|
+
"""Return the Brevo webhooks."""
|
|
312
|
+
from brevo_python import UpdateWebhook
|
|
313
|
+
wbhs = self._get_webhooks_api()
|
|
314
|
+
|
|
315
|
+
def get_webhook_sms(self, webhook_id: str):
|
|
316
|
+
"""Return the Brevo webhook."""
|
|
317
|
+
wbhs = self._get_webhooks_api()
|
|
318
|
+
return next((wbh for wbh in wbhs if str(wbh.id) == str(webhook_id)), None)
|
|
319
|
+
|
|
320
|
+
def get_webhooks_sms(self):
|
|
321
|
+
"""Return only transactional SMS webhooks."""
|
|
322
|
+
webhooks = self.get_webhooks()
|
|
323
|
+
return [webhook for webhook in webhooks if webhook["type"] == "transactional"]
|
|
324
|
+
|
|
325
|
+
def handle_webhook_sms(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
326
|
+
"""Handle a Brevo webhook and normalize event type."""
|
|
327
|
+
payload = payload.decode("utf-8")
|
|
328
|
+
payload = json.loads(payload)
|
|
329
|
+
event = payload.get("event")
|
|
330
|
+
message_id = payload.get("message-id") or payload.get("messageId")
|
|
331
|
+
return {
|
|
332
|
+
"recipient": payload.get("phone"),
|
|
333
|
+
"external_id": str(message_id),
|
|
334
|
+
"event": self.events_association.get(event, "unknown"),
|
|
335
|
+
"description": payload.get("reason"),
|
|
336
|
+
"occurred_at": payload.get("date"),
|
|
337
|
+
"trace": payload,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#########################################################
|
|
341
|
+
# Whatsapp
|
|
342
|
+
#########################################################
|
|
343
|
+
|
|
344
|
+
def _get_transactional_whatsapp_api(self):
|
|
345
|
+
"""Return the Brevo transactional WhatsApp API instance."""
|
|
346
|
+
if self._transactional_whatsapp_api is None:
|
|
347
|
+
from brevo_python import TransactionalWhatsAppApi
|
|
348
|
+
api_client = self.get_api_client(self._whatsapp_api_key)
|
|
349
|
+
self._transactional_whatsapp_api = TransactionalWhatsAppApi(api_client)
|
|
350
|
+
return self._transactional_whatsapp_api
|
|
351
|
+
|
|
352
|
+
def send_branded(self, **kwargs) -> dict[str, Any]:
|
|
353
|
+
"""Send a branded message via Brevo API."""
|
|
354
|
+
return self.send_whatsapp(**kwargs)
|
|
355
|
+
|
|
356
|
+
def send_whatsapp(self, **kwargs) -> dict[str, Any]:
|
|
357
|
+
"""Send WhatsApp via Brevo API."""
|
|
358
|
+
from brevo_python import SendWhatsappMessage
|
|
359
|
+
recipients = [str(recipient["phone"]) for recipient in kwargs.get("recipients", [])]
|
|
360
|
+
sender = "+33614397083"
|
|
361
|
+
message = SendWhatsappMessage(
|
|
362
|
+
contact_numbers=recipients,
|
|
363
|
+
sender_number=sender,
|
|
364
|
+
text=kwargs.get("body_text"),
|
|
365
|
+
)
|
|
366
|
+
api_instance = self._get_transactional_whatsapp_api()
|
|
367
|
+
response = api_instance.send_whatsapp_message(message)
|
|
368
|
+
return {field: str(getattr(response, field)) for field in response.__dict__}
|
|
369
|
+
|
|
370
|
+
def set_webhook_whatsapp(self, webhook_data: dict[str, Any]) -> bool:
|
|
371
|
+
"""Configure a webhook to receive Brevo events."""
|
|
372
|
+
from brevo_python import CreateWebhook
|
|
373
|
+
wbhs = self._get_webhooks_api()
|
|
374
|
+
create_webhook = CreateWebhook(
|
|
375
|
+
url=webhook_data.get("url"),
|
|
376
|
+
description="Missive webhook WhatsApp",
|
|
377
|
+
events=list(self.events_association.keys()),
|
|
378
|
+
channel="whatsapp",
|
|
379
|
+
type="transactional",
|
|
380
|
+
)
|
|
381
|
+
return wbhs.create_webhook(create_webhook)
|
|
382
|
+
|
|
383
|
+
def delete_webhook_whatsapp(self, webhook_data: dict[str, Any]) -> bool:
|
|
384
|
+
"""Delete a webhook from Brevo."""
|
|
385
|
+
wbhs = self._get_webhooks_api()
|
|
386
|
+
return wbhs.delete_webhook(webhook_data.get("id"))
|
|
387
|
+
|
|
388
|
+
def update_webhook_whatsapp(self, webhook_data: dict[str, Any]) -> bool:
|
|
389
|
+
"""Return the Brevo webhooks."""
|
|
390
|
+
from brevo_python import UpdateWebhook
|
|
391
|
+
wbhs = self._get_webhooks_api()
|
|
392
|
+
update = UpdateWebhook(url=webhook_data.get("url"))
|
|
393
|
+
webhook_id = webhook_data.get("id")
|
|
394
|
+
wbhs.update_webhook(webhook_id, update)
|
|
395
|
+
return self.get_normalize_webhook_id({"id": webhook_id})
|
|
396
|
+
|
|
397
|
+
def get_webhook_whatsapp(self, webhook_id: str):
|
|
398
|
+
"""Return the Brevo webhook."""
|
|
399
|
+
wbhs = self._get_webhooks_api()
|
|
400
|
+
return next((wbh for wbh in wbhs if str(wbh.id) == str(webhook_id)), None)
|
|
401
|
+
|
|
402
|
+
def get_webhooks_whatsapp(self):
|
|
403
|
+
"""Return only transactional WhatsApp webhooks."""
|
|
404
|
+
webhooks = self.get_webhooks()
|
|
405
|
+
return [webhook for webhook in webhooks if webhook["type"] == "transactional"]
|
|
406
|
+
|
|
407
|
+
def handle_webhook_whatsapp(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
408
|
+
"""Handle a Brevo webhook and normalize event type."""
|
|
409
|
+
payload = payload.decode("utf-8")
|
|
410
|
+
payload = json.loads(payload)
|
|
411
|
+
event = payload.get("event")
|
|
412
|
+
message_id = payload.get("message-id") or payload.get("messageId")
|
|
413
|
+
return {
|
|
414
|
+
"recipient": payload.get("phone"),
|
|
415
|
+
"external_id": str(message_id),
|
|
416
|
+
"event": self.events_association.get(event, "unknown"),
|
|
417
|
+
"description": payload.get("reason"),
|
|
418
|
+
"occurred_at": payload.get("date"),
|
|
419
|
+
"trace": payload,
|
|
420
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .base import MissiveProviderBase
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DjangoEmailProvider(MissiveProviderBase):
|
|
5
|
+
abstract = True
|
|
6
|
+
name = "django_email"
|
|
7
|
+
display_name = "Django Email Backend"
|
|
8
|
+
description = "Lightweight email provider delegating to SMTP or local file delivery. Mimics Django's console/backend behaviour without importing Django."
|