"""CrowData Email Service — Async email sending via SMTP.""" import logging from pathlib import Path from jinja2 import Environment, FileSystemLoader import aiosmtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication from app.config import get_settings logger = logging.getLogger(__name__) settings = get_settings() TEMPLATE_DIR = Path(__file__).parent.parent / "templates" / "emails" jinja_env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR))) async def send_email( to_email: str, subject: str, template_name: str, context: dict, attachments: list[dict] | None = None, ): """ Send an HTML email using Jinja2 templates. Args: to_email: Recipient email address subject: Email subject template_name: Template file name (e.g., 'welcome.html') context: Template context variables attachments: Optional list of dicts with 'filename' and 'content' (bytes) """ try: # Render template template = jinja_env.get_template(template_name) html_body = template.render(**context) # Build message msg = MIMEMultipart("alternative") msg["From"] = f"{settings.from_name} <{settings.from_email}>" msg["To"] = to_email msg["Subject"] = subject # Plain text fallback plain_text = _html_to_plain(html_body) msg.attach(MIMEText(plain_text, "plain", "utf-8")) msg.attach(MIMEText(html_body, "html", "utf-8")) # Attachments (e.g., PDF reports) if attachments: for att in attachments: part = MIMEApplication(att["content"], Name=att["filename"]) part["Content-Disposition"] = f'attachment; filename="{att["filename"]}"' msg.attach(part) # Send if not settings.smtp_user or not settings.smtp_password: logger.warning(f"SMTP not configured — email to {to_email} NOT sent. Subject: {subject}") return False await aiosmtplib.send( msg, hostname=settings.smtp_host, port=settings.smtp_port, username=settings.smtp_user, password=settings.smtp_password, start_tls=settings.smtp_use_tls, ) logger.info(f"Email sent to {to_email}: {subject}") return True except Exception as e: logger.error(f"Failed to send email to {to_email}: {e}") return False async def send_welcome_email(to_email: str, full_name: str | None = None): """Send welcome email after registration.""" name = full_name or to_email.split("@")[0].title() return await send_email( to_email=to_email, subject="Bienvenido a CrowData", template_name="welcome.html", context={ "name": name, "email": to_email, "login_url": "https://crowdata.ar/src/pages/login.html", "dashboard_url": "https://crowdata.ar/src/pages/dashboard.html", "support_email": settings.from_email, }, ) async def send_pdf_report(to_email: str, report_type: str, identifier: str, pdf_bytes: bytes, user_name: str | None = None): """Send a PDF report as email attachment.""" type_labels = { "persona": "Persona", "empresa": "Empresa", "vehiculo": "Vehículo", "propiedad": "Inmueble", } type_label = type_labels.get(report_type, report_type) filename = f"CrowData_{type_label}_{identifier}.pdf" return await send_email( to_email=to_email, subject=f"CrowData — Informe de {type_label} {identifier}", template_name="pdf_delivery.html", context={ "name": user_name or to_email.split("@")[0].title(), "report_type": type_label, "identifier": identifier, "support_email": settings.from_email, }, attachments=[{"filename": filename, "content": pdf_bytes}], ) def _html_to_plain(html: str) -> str: """Very basic HTML to plain text conversion.""" import re text = re.sub(r"", "\n", html) text = re.sub(r"<[^>]+>", "", text) text = re.sub(r"\s+", " ", text).strip() return text