Files
space-booking/backend/app/services/email_service.py
Claude Agent df4031d99c feat: Space Booking System - MVP complet
Sistem web pentru rezervarea de birouri și săli de ședință
cu flux de aprobare administrativă.

Stack: FastAPI + Vue.js 3 + SQLite + TypeScript

Features implementate:
- Autentificare JWT + Self-registration cu email verification
- CRUD Spații, Utilizatori, Settings (Admin)
- Calendar interactiv (FullCalendar) cu drag-and-drop
- Creare rezervări cu validare (durată, program, overlap, max/zi)
- Rezervări recurente (săptămânal)
- Admin: aprobare/respingere/anulare cereri
- Admin: creare directă rezervări (bypass approval)
- Admin: editare orice rezervare
- User: editare/anulare rezervări proprii
- Notificări in-app (bell icon + dropdown)
- Notificări email (async SMTP cu BackgroundTasks)
- Jurnal acțiuni administrative (audit log)
- Rapoarte avansate (utilizare, top users, approval rate)
- Șabloane rezervări (booking templates)
- Atașamente fișiere (upload/download)
- Conflict warnings (verificare disponibilitate real-time)
- Integrare Google Calendar (OAuth2)
- Suport timezone (UTC storage + user preference)
- 225+ teste backend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:51:29 +00:00

154 lines
4.1 KiB
Python

"""Email service for sending booking notifications."""
import logging
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Optional
import aiosmtplib
from app.core.config import settings
from app.models.booking import Booking
logger = logging.getLogger(__name__)
async def send_email(to: str, subject: str, body: str) -> bool:
"""Send email via SMTP. Returns True if successful."""
if not settings.smtp_enabled:
# Development mode: just log the email
logger.info(f"[EMAIL] To: {to}")
logger.info(f"[EMAIL] Subject: {subject}")
logger.info(f"[EMAIL] Body:\n{body}")
print(f"\n--- EMAIL ---")
print(f"To: {to}")
print(f"Subject: {subject}")
print(f"Body:\n{body}")
print(f"--- END EMAIL ---\n")
return True
try:
message = MIMEMultipart()
message["From"] = settings.smtp_from_address
message["To"] = to
message["Subject"] = subject
message.attach(MIMEText(body, "plain", "utf-8"))
await aiosmtplib.send(
message,
hostname=settings.smtp_host,
port=settings.smtp_port,
username=settings.smtp_user if settings.smtp_user else None,
password=settings.smtp_password if settings.smtp_password else None,
)
return True
except Exception as e:
logger.error(f"Failed to send email to {to}: {e}")
return False
def generate_booking_email(
booking: Booking,
event_type: str,
user_email: str,
user_name: str,
extra_data: Optional[dict] = None,
) -> tuple[str, str]:
"""Generate email subject and body for booking events.
Returns: (subject, body)
"""
extra_data = extra_data or {}
space_name = booking.space.name if booking.space else "Unknown Space"
start_str = booking.start_datetime.strftime("%d.%m.%Y %H:%M")
end_str = booking.end_datetime.strftime("%H:%M")
if event_type == "created":
subject = "Cerere Nouă de Rezervare"
body = f"""Bună ziua,
O nouă cerere de rezervare necesită aprobarea dumneavoastră:
Utilizator: {user_name}
Spațiu: {space_name}
Data și ora: {start_str} - {end_str}
Titlu: {booking.title}
Descriere: {booking.description or 'N/A'}
Vă rugăm să accesați panoul de administrare pentru a aproba sau respinge această cerere.
Cu stimă,
Sistemul de Rezervări
"""
elif event_type == "approved":
subject = "Rezervare Aprobată"
body = f"""Bună ziua {user_name},
Rezervarea dumneavoastră a fost aprobată:
Spațiu: {space_name}
Data și ora: {start_str} - {end_str}
Titlu: {booking.title}
Vă așteptăm!
Cu stimă,
Sistemul de Rezervări
"""
elif event_type == "rejected":
reason = extra_data.get("rejection_reason", "Nu a fost specificat")
subject = "Rezervare Respinsă"
body = f"""Bună ziua {user_name},
Rezervarea dumneavoastră a fost respinsă:
Spațiu: {space_name}
Data și ora: {start_str} - {end_str}
Titlu: {booking.title}
Motiv: {reason}
Vă rugăm să contactați administratorul pentru detalii.
Cu stimă,
Sistemul de Rezervări
"""
elif event_type == "canceled":
reason = extra_data.get("cancellation_reason", "Nu a fost specificat")
subject = "Rezervare Anulată"
body = f"""Bună ziua {user_name},
Rezervarea dumneavoastră a fost anulată de către administrator:
Spațiu: {space_name}
Data și ora: {start_str} - {end_str}
Titlu: {booking.title}
Motiv: {reason}
Vă rugăm să contactați administratorul pentru detalii.
Cu stimă,
Sistemul de Rezervări
"""
else:
subject = "Notificare Rezervare"
body = f"Notificare despre rezervarea pentru {space_name} din {start_str}"
return subject, body
async def send_booking_notification(
booking: Booking,
event_type: str,
user_email: str,
user_name: str,
extra_data: Optional[dict] = None,
) -> bool:
"""Send booking notification email."""
subject, body = generate_booking_email(
booking, event_type, user_email, user_name, extra_data
)
return await send_email(user_email, subject, body)