198 lines
9.9 KiB
Python
198 lines
9.9 KiB
Python
"""
|
|
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"""
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; background-color: #ffffff;">
|
|
<div style="background-color: #1e293b; padding: 20px; text-align: center; color: #ffffff;">
|
|
<h2 style="margin: 0; font-size: 20px;">Admin Edu-Space</h2>
|
|
<p style="margin: 5px 0 0 0; font-size: 14px; color: #94a3b8;">Notificación de Gestión Académica</p>
|
|
</div>
|
|
<div style="padding: 24px; color: #334155; line-height: 1.6;">
|
|
<p style="font-size: 16px;">Estimado/a <strong>{teacher_name}</strong>,</p>
|
|
<p>Le informamos que ha sido asignado/a como docente a cargo de la siguiente comisión:</p>
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #3b82f6; padding: 12px 16px; margin: 16px 0; border-radius: 4px;">
|
|
<p style="margin: 4px 0;"><strong>Materia:</strong> {subject_name}</p>
|
|
<p style="margin: 4px 0;"><strong>Comisión:</strong> {commission_name}</p>
|
|
{f'<p style="margin: 4px 0;"><strong>Horario / Aulas:</strong> {schedule}</p>' if schedule else ''}
|
|
</div>
|
|
<p>Puede consultar los detalles y la nómina de alumnos ingresando al portal de Admin Edu-Space y a las aulas de Moodle vinculadas.</p>
|
|
<div style="margin-top: 24px; text-align: center;">
|
|
<a href="http://10.0.0.217:5000/login" style="background-color: #2563eb; color: #ffffff; padding: 10px 20px; text-decoration: none; border-radius: 6px; font-weight: bold; display: inline-block;">Acceder a Edu-Space</a>
|
|
</div>
|
|
</div>
|
|
<div style="background-color: #f1f5f9; padding: 12px; text-align: center; font-size: 12px; color: #64748b;">
|
|
Este es un correo automático generado por Admin Edu-Space. Por favor no responder a esta casilla.
|
|
</div>
|
|
</div>
|
|
"""
|
|
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"""
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; background-color: #ffffff;">
|
|
<div style="background-color: #0f172a; padding: 20px; text-align: center; color: #ffffff;">
|
|
<h2 style="margin: 0; font-size: 20px;">Admin Edu-Space</h2>
|
|
<p style="margin: 5px 0 0 0; font-size: 14px; color: #cbd5e1;">Mesa de Exámenes Finales</p>
|
|
</div>
|
|
<div style="padding: 24px; color: #334155; line-height: 1.6;">
|
|
<p>Estimado/a <strong>{teacher_name}</strong>,</p>
|
|
<p>Se le ha asignado la siguiente mesa de examen final:</p>
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #10b981; padding: 12px 16px; margin: 16px 0; border-radius: 4px;">
|
|
<p style="margin: 4px 0;"><strong>Materia:</strong> {subject_name}</p>
|
|
<p style="margin: 4px 0;"><strong>Fecha y Hora:</strong> {date_str}</p>
|
|
{f'<p style="margin: 4px 0;"><strong>Espacio / Aula:</strong> {room}</p>' if room else ''}
|
|
</div>
|
|
<p>Recuerde verificar las actas y regularidades en el sistema.</p>
|
|
</div>
|
|
</div>
|
|
"""
|
|
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()
|