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, }