""" Email Notification Service (admin-edu-space) Dynamically configures and dispatches transactional emails using SMTP credentials stored securely in the database (SystemSetting) with Fernet encryption. """ import smtplib import logging from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import List, Optional, Union, Dict, Any from app.models.setting import SystemSetting logger = logging.getLogger(__name__) class EmailService: @staticmethod def get_smtp_config() -> Dict[str, Any]: """ Retrieves active SMTP configuration from database. """ host_s = SystemSetting.query.filter_by(key='smtp_host').first() port_s = SystemSetting.query.filter_by(key='smtp_port').first() user_s = SystemSetting.query.filter_by(key='smtp_user').first() pass_s = SystemSetting.query.filter_by(key='smtp_password').first() sec_s = SystemSetting.query.filter_by(key='smtp_security').first() sender_s = SystemSetting.query.filter_by(key='smtp_from_email').first() sender_name_s = SystemSetting.query.filter_by(key='smtp_from_name').first() host = host_s.value if host_s and host_s.value else 'smtp.gmail.com' port = int(port_s.value) if port_s and port_s.value else 587 user = user_s.value if user_s and user_s.value else '' password = pass_s.get_decrypted_value() if pass_s else '' security = sec_s.value if sec_s and sec_s.value else 'tls' from_email = sender_s.value if sender_s and sender_s.value else user or 'noreply@edu-space.local' from_name = sender_name_s.value if sender_name_s and sender_name_s.value else 'Admin Edu-Space' return { 'host': host, 'port': port, 'user': user, 'password': password, 'security': security.lower(), 'from_email': from_email, 'from_name': from_name } @classmethod def test_smtp_connection(cls, custom_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Tests connection and authentication with the SMTP server. """ config = custom_config or cls.get_smtp_config() host = config.get('host') port = int(config.get('port', 587)) user = config.get('user', '') password = config.get('password', '') security = config.get('security', 'tls').lower() if not host: return {'success': False, 'message': 'El Host SMTP no está especificado.'} try: if security == 'ssl' or port == 465: server = smtplib.SMTP_SSL(host, port, timeout=10) else: server = smtplib.SMTP(host, port, timeout=10) if security in ('tls', 'starttls'): server.starttls() if user and password: server.login(user, password) server.quit() return {'success': True, 'message': f'Conexión exitosa con el servidor SMTP ({host}:{port}).'} except smtplib.SMTPAuthenticationError as e: return {'success': False, 'message': f'Fallo de autenticación SMTP: Credenciales incorrectas ({e.smtp_code}).'} except smtplib.SMTPConnectError as e: return {'success': False, 'message': f'No se pudo conectar al servidor SMTP: {str(e)}'} except Exception as e: return {'success': False, 'message': f'Error de conexión SMTP: {str(e)}'} @classmethod def send_email(cls, to: Union[str, List[str]], subject: str, html_content: str, text_content: Optional[str] = None) -> bool: """ Sends an email using dynamic SMTP configuration. """ config = cls.get_smtp_config() host = config.get('host') port = config.get('port', 587) user = config.get('user', '') password = config.get('password', '') security = config.get('security', 'tls').lower() from_email = config.get('from_email') from_name = config.get('from_name') recipients = [to] if isinstance(to, str) else to if not recipients or not recipients[0]: logger.warning("[EmailService] No recipients specified. Aborting send.") return False msg = MIMEMultipart('alternative') msg['Subject'] = subject msg['From'] = f"{from_name} <{from_email}>" msg['To'] = ", ".join(recipients) if text_content: msg.attach(MIMEText(text_content, 'plain', 'utf-8')) if html_content: msg.attach(MIMEText(html_content, 'html', 'utf-8')) try: if security == 'ssl' or port == 465: server = smtplib.SMTP_SSL(host, port, timeout=15) else: server = smtplib.SMTP(host, port, timeout=15) if security in ('tls', 'starttls'): server.starttls() if user and password: server.login(user, password) server.sendmail(from_email, recipients, msg.as_string()) server.quit() logger.info(f"[EmailService] Email sent successfully to {recipients}: '{subject}'") return True except Exception as e: logger.error(f"[EmailService] Failed to send email to {recipients}: {e}") return False # ------------------------------------------------------------- # Casos de Uso Core: Correos Transaccionales para Profesores/Admin # ------------------------------------------------------------- @classmethod def notify_teacher_assignment(cls, teacher_email: str, teacher_name: str, subject_name: str, commission_name: str, schedule: str = "") -> bool: """ Notifica a un profesor sobre la asignación a una comisión/materia. """ subject = f"Asignación Docente: {subject_name} ({commission_name})" html = f"""
""" text = f"Estimado/a {teacher_name},\n\nHa sido asignado/a a la materia: {subject_name} ({commission_name}). Horario: {schedule}.\n\nAcceda a Edu-Space para más información." return cls.send_email(teacher_email, subject, html, text) @classmethod def notify_exam_schedule(cls, teacher_email: str, teacher_name: str, subject_name: str, date_str: str, room: str = "") -> bool: """ Notifica a un profesor sobre la mesa examinadora asignada. """ subject = f"Mesa de Examen Asignada: {subject_name} - {date_str}" html = f""" """ text = f"Estimado/a {teacher_name},\n\nMesa de examen asignada: {subject_name}\nFecha: {date_str}\nAula: {room}" return cls.send_email(teacher_email, subject, html, text) email_service = EmailService()