"""Email service using aiosmtplib for contact and rental confirmation emails. Includes a failed-email queue: when SMTP fails and a db session is provided, the email is persisted to the email_queue table for later retry via APScheduler. """ import logging from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from typing import Any from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession import aiosmtplib from app.config import get_settings from app.models.email_queue import EmailQueue logger = logging.getLogger(__name__) settings = get_settings() class EmailService: """Send transactional emails via SMTP with failed-email queue support.""" async def send_email( self, to_addr: str, subject: str, html_body: str, text_body: str = "", reply_to: str | None = None, db: AsyncSession | None = None, ) -> bool: """Send an email via aiosmtplib. Returns True on success. If SMTP fails and a db session is provided, the email is saved to the email_queue table for later retry. """ msg = MIMEMultipart("alternative") msg["From"] = settings.smtp_from msg["To"] = to_addr msg["Subject"] = subject if reply_to: msg["Reply-To"] = reply_to msg.attach(MIMEText(text_body or html_body, "plain")) msg.attach(MIMEText(html_body, "html")) try: await aiosmtplib.send( msg, hostname=settings.smtp_host, port=settings.smtp_port, username=settings.smtp_user, password=settings.smtp_password, start_tls=True, ) return True except Exception as exc: logger.error("Email send failed to %s: %s", to_addr, exc) if db is not None: try: queued = EmailQueue( to_addr=to_addr, subject=subject, html_body=html_body, text_body=text_body, reply_to=reply_to, status="pending", attempts=0, ) db.add(queued) await db.commit() logger.info("Email queued for retry: %s", to_addr) except Exception as queue_exc: logger.error("Failed to queue email: %s", queue_exc) return False async def send_contact_email( self, contact_data: dict[str, Any], db: AsyncSession | None = None ) -> bool: """Send contact form data to info@hms-licht-ton.de.""" html = f"""
Name: {contact_data.get('name', '')}
Email: {contact_data.get('email', '')}
Telefon: {contact_data.get('phone', '')}
Nachricht:
{contact_data.get('message', '')}
""" text = ( f"Neue Kontaktanfrage\n" f"Name: {contact_data.get('name', '')}\n" f"Email: {contact_data.get('email', '')}\n" f"Telefon: {contact_data.get('phone', '')}\n" f"Nachricht: {contact_data.get('message', '')}\n" ) return await self.send_email( to_addr=settings.smtp_from, subject="Neue Kontaktanfrage ueber hms-licht-ton.de", html_body=html, text_body=text, reply_to=contact_data.get("email"), db=db, ) async def send_rental_confirmation( self, to_addr: str, reference_number: str, event_name: str, items: list[dict[str, Any]], db: AsyncSession | None = None, ) -> bool: """Send rental request confirmation to customer.""" items_html = "".join( f"Vielen Dank für Ihre Mietanfrage bei HMS Licht & Ton.
Veranstaltung: {event_name}
Referenznummer: {reference_number}
Wir melden uns in Kürze bei Ihnen.
Ihr HMS Licht & Ton Team
""" text = ( f"Mietanfrage bestaetigt - {reference_number}\n" f"Veranstaltung: {event_name}\n" f"Referenznummer: {reference_number}\n" f"Artikel:\n" + "\n".join( f"- {item.get('equipment_name', 'Unbekannt')}: {item.get('quantity', 1)}" for item in items ) ) return await self.send_email( to_addr=to_addr, subject=f"Mietanfrage bestaetigt – {reference_number}", html_body=html, text_body=text, db=db, ) @staticmethod async def retry_failed_emails(db: AsyncSession, max_attempts: int = 5) -> int: """Retry all pending emails in the queue. Returns count of successfully sent emails.""" result = await db.execute( select(EmailQueue).where( EmailQueue.status == "pending", EmailQueue.attempts < max_attempts, ) ) pending = result.scalars().all() sent_count = 0 for item in pending: item.attempts += 1 msg = MIMEMultipart("alternative") msg["From"] = settings.smtp_from msg["To"] = item.to_addr msg["Subject"] = item.subject if item.reply_to: msg["Reply-To"] = item.reply_to msg.attach(MIMEText(item.text_body or item.html_body, "plain")) msg.attach(MIMEText(item.html_body, "html")) try: await aiosmtplib.send( msg, hostname=settings.smtp_host, port=settings.smtp_port, username=settings.smtp_user, password=settings.smtp_password, start_tls=True, ) item.status = "sent" sent_count += 1 logger.info("Retry succeeded for email to %s", item.to_addr) except Exception as exc: logger.error("Retry failed for email to %s: %s", item.to_addr, exc) if item.attempts >= max_attempts: item.status = "failed" await db.commit() return sent_count