""" Async SMTP Email Service with retry logic and proper error handling """ import aiosmtplib from email.message import EmailMessage import os import logging from typing import Optional import asyncio logger = logging.getLogger(__name__) class EmailService: """Async SMTP client for sending authentication codes""" def __init__(self): self.smtp_host = os.getenv("SMTP_HOST", "mail.romfast.ro") self.smtp_port = int(os.getenv("SMTP_PORT", "587")) self.smtp_user = os.getenv("SMTP_USER") self.smtp_password = os.getenv("SMTP_PASSWORD") self.from_email = os.getenv("SMTP_FROM_EMAIL") self.from_name = os.getenv("SMTP_FROM_NAME", "ROA2WEB") self.use_tls = os.getenv("SMTP_USE_TLS", "true").lower() == "true" # Retry configuration self.max_retries = int(os.getenv("EMAIL_MAX_RETRIES", "3")) self.retry_delay = float(os.getenv("EMAIL_RETRY_DELAY", "2.0")) # seconds # Validate required config if not all([self.smtp_user, self.smtp_password, self.from_email]): raise ValueError("SMTP configuration incomplete. Check .env file.") async def send_auth_code( self, to_email: str, code: str, username: str ) -> bool: """ Send authentication code via email with retry logic Args: to_email: Recipient email address code: 6-digit authentication code username: Oracle username for personalization Returns: True if email sent successfully (after retries if needed) Raises: No exceptions - returns False on all failures """ subject = "Autentificare ROA2WEB" text_body = self._create_email_template(code, username) for attempt in range(1, self.max_retries + 1): try: await self._send_email(to_email, subject, text_body) logger.info( f"[EMAIL] ✅ Sent auth code to {to_email} " f"(attempt {attempt}/{self.max_retries}) via {self.smtp_host}:{self.smtp_port}" ) return True except aiosmtplib.SMTPException as e: logger.error( f"[EMAIL] ❌ Attempt {attempt}/{self.max_retries} failed for {to_email}: " f"{type(e).__name__}: {e}" ) if attempt < self.max_retries: # Exponential backoff: 2s, 4s, 8s delay = self.retry_delay * (2 ** (attempt - 1)) logger.info(f"[EMAIL] Retrying in {delay}s...") await asyncio.sleep(delay) else: logger.error(f"[EMAIL] ❌ All {self.max_retries} attempts failed for {to_email}") except Exception as e: logger.error(f"[EMAIL] ❌ Unexpected error on attempt {attempt}/{self.max_retries} for {to_email}: {type(e).__name__}: {e}", exc_info=True) return False return False async def _send_email( self, to_email: str, subject: str, text_body: str ) -> None: """ Internal async SMTP sender (plain text to avoid spam filters) Raises: aiosmtplib.SMTPException: On SMTP errors """ message = EmailMessage() message["From"] = f"{self.from_name} <{self.from_email}>" message["To"] = to_email message["Subject"] = subject message.set_content(text_body) smtp = aiosmtplib.SMTP( hostname=self.smtp_host, port=self.smtp_port, start_tls=self.use_tls, timeout=30 ) try: await smtp.connect() await smtp.login(self.smtp_user, self.smtp_password) await smtp.send_message(message) finally: try: await smtp.quit() except: pass def _create_email_template(self, code: str, username: str) -> str: """Generate plain text email body (HTML blocked by spam filters)""" return ( f"Codul tau de autentificare ROA2WEB:\n\n" f" {code}\n\n" f"Introdu acest cod in Telegram. Expira in 5 minute.\n\n" f"---\n" f"Solicitat pentru: {username}\n" f"Daca nu ai initiat aceasta autentificare, ignora acest email." ) # Singleton instance _email_service: Optional[EmailService] = None def get_email_service() -> EmailService: """Get or create singleton email service instance""" global _email_service if _email_service is None: _email_service = EmailService() return _email_service