From ba4a73d5cab6920f34ff69eec60aaeb7045de864 Mon Sep 17 00:00:00 2001 From: allebonvi Date: Fri, 4 Sep 2026 15:46:18 +0200 Subject: [PATCH] Aggiunge invio prenotazioni con Mailjet --- backend/.env.example | 14 ++++ backend/app/config.py | 44 +++++++++--- backend/app/services/mailer.py | 120 ++++++++++++++++++++++++++++++--- 3 files changed, 158 insertions(+), 20 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index c2e92bd..f2837c9 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,11 +3,25 @@ APP_ENV=development APP_HOST=127.0.0.1 APP_PORT=8000 DATABASE_URL=sqlite:///C:/devel/clinica_veterinaria_formiginese/data/clinica.db + +MAIL_PROVIDER=mailjet +MAILJET_API_KEY= +MAILJET_API_SECRET= +MAILJET_API_URL=https://api.mailjet.com/v3.1/send +MAILJET_SANDBOX_MODE=true + SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USERNAME= SMTP_PASSWORD= SMTP_USE_TLS=true BOOKING_EMAIL_FROM= +BOOKING_EMAIL_FROM_NAME=Clinica Veterinaria Formiginese BOOKING_EMAIL_TO= BOOKING_EMAIL_SUBJECT_PREFIX=[Clinica Veterinaria Formiginese] +BOOKING_FORM_ENABLED=false +BOOKING_AUDIT_LOG_PATH= +BOOKING_RATE_LIMIT_WINDOW_SECONDS=900 +BOOKING_RATE_LIMIT_MAX_REQUESTS=5 +BOOKING_CAPTCHA_TTL_SECONDS=600 +BOOKING_CAPTCHA_SECRET= diff --git a/backend/app/config.py b/backend/app/config.py index ad92e6d..72400f3 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -27,11 +27,22 @@ class Settings: self.smtp_password = os.getenv("SMTP_PASSWORD", "") self.smtp_use_tls = os.getenv("SMTP_USE_TLS", "true").lower() in {"1", "true", "yes", "on"} self.booking_email_from = os.getenv("BOOKING_EMAIL_FROM", self.smtp_username) + self.booking_email_from_name = os.getenv("BOOKING_EMAIL_FROM_NAME", "Clinica Veterinaria Formiginese") self.booking_email_to = os.getenv("BOOKING_EMAIL_TO", "") self.booking_email_subject_prefix = os.getenv( "BOOKING_EMAIL_SUBJECT_PREFIX", "[Clinica Veterinaria Formiginese]", ) + self.mail_provider = os.getenv("MAIL_PROVIDER", "mailjet").lower() + self.mailjet_api_key = os.getenv("MAILJET_API_KEY", "") + self.mailjet_api_secret = os.getenv("MAILJET_API_SECRET", "") + self.mailjet_api_url = os.getenv("MAILJET_API_URL", "https://api.mailjet.com/v3.1/send") + self.mailjet_sandbox_mode = os.getenv("MAILJET_SANDBOX_MODE", "false").lower() in { + "1", + "true", + "yes", + "on", + } self.booking_form_enabled = os.getenv("BOOKING_FORM_ENABLED", "false").lower() in { "1", "true", @@ -59,16 +70,29 @@ class Settings: @property def booking_mail_enabled(self) -> bool: - return all( - [ - self.smtp_host, - self.smtp_port, - self.smtp_username, - self.smtp_password, - self.booking_email_from, - self.booking_email_to, - ] - ) + if self.mail_provider == "mailjet": + return all( + [ + self.mailjet_api_key, + self.mailjet_api_secret, + self.booking_email_from, + self.booking_email_to, + ] + ) + + if self.mail_provider == "smtp": + return all( + [ + self.smtp_host, + self.smtp_port, + self.smtp_username, + self.smtp_password, + self.booking_email_from, + self.booking_email_to, + ] + ) + + return False settings = Settings() diff --git a/backend/app/services/mailer.py b/backend/app/services/mailer.py index 286d36e..fa29845 100644 --- a/backend/app/services/mailer.py +++ b/backend/app/services/mailer.py @@ -1,8 +1,13 @@ 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 @@ -12,28 +17,42 @@ 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"""

Nuova richiesta di prenotazione visita

È stata inviata una nuova richiesta di prenotazione non vincolante dal sito web.

- - - - - - - - - - + + + + + + + + + +
Nome e cognome{payload.name}
Email{payload.email}
Telefono{payload.phone}
Nome animale{payload.pet_name or "-"}
Tipo animale{payload.pet_type}
Medico richiesto{payload.doctor}
Tipo di visita{payload.service}
Periodo preferito{payload.date}
Fascia oraria preferita{payload.time or "Qualsiasi orario"}
Note{payload.notes or "-"}
Nome e cognome{name}
Email{email}
Telefono{phone}
Nome animale{pet_name}
Tipo animale{pet_type}
Medico richiesto{doctor}
Tipo di visita{service}
Periodo preferito{date}
Fascia oraria preferita{time}
Note{notes}
Inviata il{submitted_at}

@@ -65,6 +84,21 @@ def _build_booking_text(payload: BookingRequestCreate) -> str: 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.") @@ -81,3 +115,69 @@ def send_booking_request_email(payload: BookingRequestCreate) -> None: 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]}")