""" 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"""

Admin Edu-Space

Notificación de Gestión Académica

Estimado/a {teacher_name},

Le informamos que ha sido asignado/a como docente a cargo de la siguiente comisión:

Materia: {subject_name}

Comisión: {commission_name}

{f'

Horario / Aulas: {schedule}

' if schedule else ''}

Puede consultar los detalles y la nómina de alumnos ingresando al portal de Admin Edu-Space y a las aulas de Moodle vinculadas.

Acceder a Edu-Space
Este es un correo automático generado por Admin Edu-Space. Por favor no responder a esta casilla.
""" 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"""

Admin Edu-Space

Mesa de Exámenes Finales

Estimado/a {teacher_name},

Se le ha asignado la siguiente mesa de examen final:

Materia: {subject_name}

Fecha y Hora: {date_str}

{f'

Espacio / Aula: {room}

' if room else ''}

Recuerde verificar las actas y regularidades en el sistema.

""" 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()