email.py 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. import logging
  2. from abc import ABC, abstractmethod
  3. from email.message import EmailMessage
  4. import aiosmtplib
  5. from app.core.config import settings
  6. logger = logging.getLogger(__name__)
  7. class EmailSender(ABC):
  8. @abstractmethod
  9. async def send(self, to: str, subject: str, html_body: str) -> None: ...
  10. class LoggingEmailSender(EmailSender):
  11. """Dev/test fallback: logs instead of sending. Wire a real provider
  12. (SES, Postmark, SMTP) behind this interface before going to prod."""
  13. async def send(self, to: str, subject: str, html_body: str) -> None:
  14. logger.info(f"[EMAIL to={to}] {subject}\n{html_body}")
  15. class SmtpEmailSender(EmailSender):
  16. """Sends via SMTP. Works with any provider that exposes SMTP
  17. credentials (SES, Postmark, Sendgrid, a plain mailbox, etc)."""
  18. async def send(self, to: str, subject: str, html_body: str) -> None:
  19. message = EmailMessage()
  20. message["From"] = f"{settings.EMAIL_FROM_NAME} <{settings.EMAIL_FROM_ADDRESS}>"
  21. message["To"] = to
  22. message["Subject"] = subject
  23. message.set_content("Ce message nécessite un client compatible HTML.")
  24. message.add_alternative(html_body, subtype="html")
  25. try:
  26. await aiosmtplib.send(
  27. message,
  28. hostname=settings.SMTP_HOST,
  29. port=settings.SMTP_PORT,
  30. username=settings.SMTP_USERNAME or None,
  31. password=settings.SMTP_PASSWORD or None,
  32. start_tls=settings.SMTP_USE_TLS,
  33. timeout=10,
  34. )
  35. except (aiosmtplib.SMTPException, OSError) as exc:
  36. # Don't let a transient SMTP outage 500 the caller's whole
  37. # request (e.g. invite-member already committed the DB write) --
  38. # log loudly so it's visible in monitoring, and let the caller
  39. # decide whether to surface a degraded-but-successful response.
  40. logger.error("Failed to send email to %s: %s", to, exc)
  41. raise EmailDeliveryError(str(exc)) from exc
  42. class EmailDeliveryError(Exception):
  43. pass
  44. def get_email_sender() -> EmailSender:
  45. if settings.ENVIRONMENT == "PYTEST" or not settings.SMTP_HOST:
  46. return LoggingEmailSender()
  47. return SmtpEmailSender()