Files
clinica-veterinaria-formigi…/backend/app/services/mailer.py

184 lines
7.1 KiB
Python

from __future__ import annotations
import base64
from datetime import datetime
from email.message import EmailMessage
from html import escape
import json
import smtplib
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from app.config import settings
from app.schemas.booking import BookingRequestCreate
class MailConfigurationError(RuntimeError):
pass
class MailDeliveryError(RuntimeError):
pass
def _build_booking_subject(payload: BookingRequestCreate) -> str:
return f"{settings.booking_email_subject_prefix} Nuova richiesta visita - {payload.name}"
def _build_booking_html(payload: BookingRequestCreate) -> str:
submitted_at = datetime.now().strftime("%d/%m/%Y %H:%M")
name = escape(payload.name)
email = escape(payload.email)
phone = escape(payload.phone)
pet_name = escape(payload.pet_name or "-")
pet_type = escape(payload.pet_type)
doctor = escape(payload.doctor)
service = escape(payload.service)
date = escape(payload.date)
time = escape(payload.time or "Qualsiasi orario")
notes = escape(payload.notes or "-")
return f"""
<html>
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.6;">
<h2 style="color: #1B4F72;">Nuova richiesta di prenotazione visita</h2>
<p>È stata inviata una nuova richiesta di prenotazione non vincolante dal sito web.</p>
<table cellpadding="8" cellspacing="0" border="0" style="border-collapse: collapse;">
<tr><td><strong>Nome e cognome</strong></td><td>{name}</td></tr>
<tr><td><strong>Email</strong></td><td>{email}</td></tr>
<tr><td><strong>Telefono</strong></td><td>{phone}</td></tr>
<tr><td><strong>Nome animale</strong></td><td>{pet_name}</td></tr>
<tr><td><strong>Tipo animale</strong></td><td>{pet_type}</td></tr>
<tr><td><strong>Medico richiesto</strong></td><td>{doctor}</td></tr>
<tr><td><strong>Tipo di visita</strong></td><td>{service}</td></tr>
<tr><td><strong>Periodo preferito</strong></td><td>{date}</td></tr>
<tr><td><strong>Fascia oraria preferita</strong></td><td>{time}</td></tr>
<tr><td><strong>Note</strong></td><td>{notes}</td></tr>
<tr><td><strong>Inviata il</strong></td><td>{submitted_at}</td></tr>
</table>
<p style="margin-top: 20px;">
Questa richiesta <strong>non costituisce conferma automatica dell'appuntamento</strong> e richiede
presa in carico da parte dello staff.
</p>
</body>
</html>
"""
def _build_booking_text(payload: BookingRequestCreate) -> str:
submitted_at = datetime.now().strftime("%d/%m/%Y %H:%M")
return (
"Nuova richiesta di prenotazione visita\n\n"
f"Nome e cognome: {payload.name}\n"
f"Email: {payload.email}\n"
f"Telefono: {payload.phone}\n"
f"Nome animale: {payload.pet_name or '-'}\n"
f"Tipo animale: {payload.pet_type}\n"
f"Medico richiesto: {payload.doctor}\n"
f"Tipo di visita: {payload.service}\n"
f"Periodo preferito: {payload.date}\n"
f"Fascia oraria preferita: {payload.time or 'Qualsiasi orario'}\n"
f"Note: {payload.notes or '-'}\n"
f"Inviata il: {submitted_at}\n\n"
"La richiesta non costituisce conferma automatica dell'appuntamento."
)
def send_booking_request_email(payload: BookingRequestCreate) -> None:
if not settings.booking_mail_enabled:
raise MailConfigurationError(f"Configurazione email incompleta per provider {settings.mail_provider}.")
if settings.mail_provider == "mailjet":
_send_booking_request_email_mailjet(payload)
return
if settings.mail_provider == "smtp":
_send_booking_request_email_smtp(payload)
return
raise MailConfigurationError(f"Provider email non supportato: {settings.mail_provider}")
def _send_booking_request_email_smtp(payload: BookingRequestCreate) -> None:
if not settings.booking_mail_enabled:
raise MailConfigurationError("Configurazione SMTP incompleta.")
message = EmailMessage()
message["Subject"] = _build_booking_subject(payload)
message["From"] = settings.booking_email_from
message["To"] = settings.booking_email_to
message["Reply-To"] = payload.email
message.set_content(_build_booking_text(payload))
message.add_alternative(_build_booking_html(payload), subtype="html")
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=30) as server:
if settings.smtp_use_tls:
server.starttls()
server.login(settings.smtp_username, settings.smtp_password)
server.send_message(message)
def _send_booking_request_email_mailjet(payload: BookingRequestCreate) -> None:
mailjet_payload = {
"Messages": [
{
"From": {
"Email": settings.booking_email_from,
"Name": settings.booking_email_from_name,
},
"To": [
{
"Email": settings.booking_email_to,
"Name": "Clinica Veterinaria Formiginese",
}
],
"ReplyTo": {
"Email": payload.email,
"Name": payload.name,
},
"Subject": _build_booking_subject(payload),
"TextPart": _build_booking_text(payload),
"HTMLPart": _build_booking_html(payload),
}
]
}
if settings.mailjet_sandbox_mode:
mailjet_payload["SandboxMode"] = True
auth_token = base64.b64encode(
f"{settings.mailjet_api_key}:{settings.mailjet_api_secret}".encode("utf-8")
).decode("ascii")
request = Request(
settings.mailjet_api_url,
data=json.dumps(mailjet_payload).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Basic {auth_token}",
},
method="POST",
)
try:
with urlopen(request, timeout=30) as response:
response_body = response.read().decode("utf-8")
response_status = response.status
except HTTPError as exc:
error_body = exc.read().decode("utf-8", errors="replace")
raise MailDeliveryError(f"Mailjet API HTTP {exc.code}: {error_body[:500]}") from exc
except URLError as exc:
raise MailDeliveryError(f"Mailjet API connection error: {exc.reason}") from exc
if response_status < 200 or response_status >= 300:
raise MailDeliveryError(f"Mailjet API HTTP {response_status}: {response_body[:500]}")
try:
response_payload = json.loads(response_body)
except json.JSONDecodeError as exc:
raise MailDeliveryError(f"Mailjet API response non JSON: {response_body[:500]}") from exc
messages = response_payload.get("Messages", [])
message_status = messages[0].get("Status") if messages else None
if message_status != "success":
raise MailDeliveryError(f"Mailjet API delivery status {message_status}: {response_body[:500]}")