You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
199 lines
6.6 KiB
Python
199 lines
6.6 KiB
Python
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from app.core.settings import settings
|
|
|
|
|
|
class IntegrationProviderError(RuntimeError):
|
|
"""Erro de transporte ou configuracao em providers externos."""
|
|
|
|
|
|
def _clean_text(value: Any) -> str | None:
|
|
text = str(value or "").strip()
|
|
return text or None
|
|
|
|
|
|
def _normalize_address(value: Any, *, fallback_name: Any = None) -> dict[str, str] | None:
|
|
if isinstance(value, Mapping):
|
|
email = _clean_text(value.get("email"))
|
|
name = _clean_text(value.get("name"))
|
|
else:
|
|
email = _clean_text(value)
|
|
name = _clean_text(fallback_name)
|
|
|
|
if not email:
|
|
return None
|
|
|
|
address = {"email": email}
|
|
if name:
|
|
address["name"] = name
|
|
return address
|
|
|
|
|
|
def _normalize_address_list(value: Any) -> list[dict[str, str]]:
|
|
if value is None:
|
|
return []
|
|
|
|
candidates = value if isinstance(value, (list, tuple, set)) else [value]
|
|
addresses: list[dict[str, str]] = []
|
|
for candidate in candidates:
|
|
address = _normalize_address(candidate)
|
|
if address:
|
|
addresses.append(address)
|
|
return addresses
|
|
|
|
|
|
def _normalize_tags(*groups: Any) -> list[str]:
|
|
merged: list[str] = []
|
|
for group in groups:
|
|
if group is None:
|
|
continue
|
|
candidates = group if isinstance(group, (list, tuple, set)) else [group]
|
|
for candidate in candidates:
|
|
tag = _clean_text(candidate)
|
|
if tag and tag not in merged:
|
|
merged.append(tag)
|
|
return merged
|
|
|
|
|
|
def _normalize_headers(value: Any) -> dict[str, str]:
|
|
if not isinstance(value, Mapping):
|
|
return {}
|
|
|
|
headers: dict[str, str] = {}
|
|
for key, header_value in value.items():
|
|
normalized_key = _clean_text(key)
|
|
normalized_value = _clean_text(header_value)
|
|
if normalized_key and normalized_value:
|
|
headers[normalized_key] = normalized_value
|
|
return headers
|
|
|
|
|
|
class BrevoEmailProvider:
|
|
provider_name = "brevo_email"
|
|
|
|
def __init__(self) -> None:
|
|
self.base_url = str(settings.brevo_base_url or "https://api.brevo.com/v3").rstrip("/")
|
|
self.api_key = str(settings.brevo_api_key or "").strip()
|
|
self.sender_email = str(settings.brevo_sender_email or "").strip()
|
|
self.sender_name = str(settings.brevo_sender_name or "Orquestrador").strip() or "Orquestrador"
|
|
self.timeout_seconds = max(1, int(settings.brevo_request_timeout_seconds or 10))
|
|
|
|
def _normalize_provider_config(self, provider_config: Mapping[str, Any] | None) -> dict[str, Any]:
|
|
if not isinstance(provider_config, Mapping):
|
|
return {}
|
|
return dict(provider_config)
|
|
|
|
def _resolve_sender(self, provider_config: Mapping[str, Any]) -> tuple[str | None, str]:
|
|
sender = provider_config.get("sender")
|
|
sender_payload = _normalize_address(sender) if sender is not None else None
|
|
sender_email = (
|
|
(sender_payload or {}).get("email")
|
|
or _clean_text(provider_config.get("sender_email"))
|
|
or self.sender_email
|
|
)
|
|
sender_name = (
|
|
(sender_payload or {}).get("name")
|
|
or _clean_text(provider_config.get("sender_name"))
|
|
or self.sender_name
|
|
)
|
|
return sender_email, sender_name or "Orquestrador"
|
|
|
|
def is_configured(self, provider_config: Mapping[str, Any] | None = None) -> bool:
|
|
normalized_provider_config = self._normalize_provider_config(provider_config)
|
|
sender_email, _sender_name = self._resolve_sender(normalized_provider_config)
|
|
return bool(self.api_key and sender_email)
|
|
|
|
async def send_email(
|
|
self,
|
|
*,
|
|
to_email: str,
|
|
to_name: str | None,
|
|
subject: str,
|
|
body: str,
|
|
tags: list[str] | None = None,
|
|
provider_config: Mapping[str, Any] | None = None,
|
|
) -> dict:
|
|
normalized_provider_config = self._normalize_provider_config(provider_config)
|
|
sender_email, sender_name = self._resolve_sender(normalized_provider_config)
|
|
if not self.is_configured(normalized_provider_config):
|
|
raise IntegrationProviderError(
|
|
"Brevo nao configurado. Defina BREVO_API_KEY e BREVO_SENDER_EMAIL para enviar emails."
|
|
)
|
|
|
|
payload = {
|
|
"sender": {
|
|
"email": sender_email,
|
|
"name": sender_name,
|
|
},
|
|
"to": [
|
|
{
|
|
"email": str(to_email or "").strip(),
|
|
**({"name": str(to_name).strip()} if str(to_name or "").strip() else {}),
|
|
}
|
|
],
|
|
"subject": str(subject or "").strip(),
|
|
"textContent": str(body or "").strip(),
|
|
}
|
|
|
|
reply_to = _normalize_address(
|
|
normalized_provider_config.get("reply_to") or normalized_provider_config.get("reply_to_email"),
|
|
fallback_name=normalized_provider_config.get("reply_to_name"),
|
|
)
|
|
if reply_to:
|
|
payload["replyTo"] = reply_to
|
|
|
|
cc = _normalize_address_list(normalized_provider_config.get("cc"))
|
|
if cc:
|
|
payload["cc"] = cc
|
|
|
|
bcc = _normalize_address_list(normalized_provider_config.get("bcc"))
|
|
if bcc:
|
|
payload["bcc"] = bcc
|
|
|
|
merged_tags = _normalize_tags(tags, normalized_provider_config.get("tags"))
|
|
if merged_tags:
|
|
payload["tags"] = merged_tags
|
|
|
|
headers = _normalize_headers(normalized_provider_config.get("headers"))
|
|
if headers:
|
|
payload["headers"] = headers
|
|
|
|
html_content = _clean_text(normalized_provider_config.get("html_content"))
|
|
if html_content:
|
|
payload["htmlContent"] = html_content
|
|
|
|
headers = {
|
|
"accept": "application/json",
|
|
"content-type": "application/json",
|
|
"api-key": self.api_key,
|
|
}
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
|
response = await client.post(
|
|
f"{self.base_url}/smtp/email",
|
|
headers=headers,
|
|
json=payload,
|
|
)
|
|
except httpx.HTTPError as exc:
|
|
raise IntegrationProviderError(f"Falha ao enviar email via Brevo: {exc}") from exc
|
|
|
|
if response.status_code >= 400:
|
|
raise IntegrationProviderError(
|
|
f"Brevo retornou erro {response.status_code}: {response.text[:300]}"
|
|
)
|
|
|
|
try:
|
|
data = response.json()
|
|
except ValueError:
|
|
data = {}
|
|
|
|
return {
|
|
"provider": self.provider_name,
|
|
"message_id": data.get("messageId"),
|
|
"response": data,
|
|
}
|