""" Service d'envoi d'emails pour Notytex v2. Gère la configuration SMTP et l'envoi de bilans d'évaluation. """ import smtplib import logging import re from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from typing import List, Optional, Dict, Any from dataclasses import dataclass @dataclass class SMTPConfig: """Configuration SMTP pour l'envoi d'emails.""" host: str port: int username: str password: str use_tls: bool from_name: str from_address: str def is_valid(self) -> bool: """Vérifie si la configuration est valide pour l'envoi.""" # Vérifier les champs obligatoires if not self.host or not self.from_address: return False # Pour les serveurs locaux de test, l'authentification n'est pas requise is_localhost = self.host.lower() in ['localhost', '127.0.0.1'] is_test_port = str(self.port) in ['1025', '2525', '8025'] if not is_localhost or not is_test_port: # Pour les vrais serveurs SMTP, username et password sont requis if not self.username or not self.password: return False return True class EmailService: """Service d'envoi d'emails avec configuration dynamique.""" def __init__(self, smtp_config: Optional[SMTPConfig] = None): """ Initialise le service avec la configuration SMTP. Args: smtp_config: Configuration SMTP (optionnelle, peut être définie plus tard) """ self._config = smtp_config self.logger = logging.getLogger(__name__) def set_config(self, config: SMTPConfig) -> None: """ Définit la configuration SMTP. Args: config: Configuration SMTP """ self._config = config @property def config(self) -> Optional[SMTPConfig]: """Retourne la configuration actuelle.""" return self._config def is_configured(self) -> bool: """Vérifie si la configuration email est complète.""" if not self._config: return False return self._config.is_valid() def send_email( self, to_emails: List[str], subject: str, html_body: str, text_body: Optional[str] = None ) -> Dict[str, Any]: """ Envoie un email à une liste de destinataires. Args: to_emails: Liste des adresses email destinataires subject: Sujet de l'email html_body: Corps de l'email en HTML text_body: Corps de l'email en texte brut (optionnel) Returns: Dict avec le statut de l'envoi et les détails """ if not self.is_configured(): return { 'success': False, 'error': 'Configuration email incomplète. Vérifiez les paramètres SMTP.' } config = self._config try: # Préparer l'email msg = MIMEMultipart('alternative') msg['Subject'] = subject msg['From'] = f"{config.from_name} <{config.from_address}>" msg['To'] = ', '.join(to_emails) # Ajouter le corps en texte brut si fourni if text_body: part1 = MIMEText(text_body, 'plain', 'utf-8') msg.attach(part1) # Ajouter le corps HTML # Note: premailer transform() peut être ajouté ici si nécessaire part2 = MIMEText(html_body, 'html', 'utf-8') msg.attach(part2) # Connexion SMTP et envoi with smtplib.SMTP(config.host, config.port) as server: if config.use_tls: server.starttls() # Ne pas s'authentifier sur les serveurs de test locaux is_localhost = config.host.lower() in ['localhost', '127.0.0.1'] is_test_port = str(config.port) in ['1025', '2525', '8025'] if not (is_localhost and is_test_port) and config.username and config.password: server.login(config.username, config.password) server.send_message(msg) self.logger.info(f"Email envoyé avec succès à {len(to_emails)} destinataires: {subject}") return { 'success': True, 'message': f'Email envoyé avec succès à {len(to_emails)} destinataire(s)', 'recipients_count': len(to_emails) } except smtplib.SMTPAuthenticationError as e: error_msg = "Erreur d'authentification SMTP. Vérifiez les identifiants." self.logger.error(f"Erreur SMTP Auth: {e}") return {'success': False, 'error': error_msg} except smtplib.SMTPException as e: error_msg = f"Erreur SMTP lors de l'envoi: {str(e)}" self.logger.error(f"Erreur SMTP: {e}") return {'success': False, 'error': error_msg} except Exception as e: error_msg = f"Erreur inattendue lors de l'envoi: {str(e)}" self.logger.error(f"Erreur envoi email: {e}") return {'success': False, 'error': error_msg} def send_test_email(self, to_email: str) -> Dict[str, Any]: """ Envoie un email de test pour vérifier la configuration. Args: to_email: Adresse email de test Returns: Dict avec le statut du test """ subject = "Test de configuration email - Notytex" html_body = """

Test de configuration email

Félicitations ! Votre configuration email fonctionne correctement.

Vous pouvez maintenant envoyer des bilans d'évaluation par email.


Email envoyé depuis Notytex - Système de gestion scolaire

""" text_body = """ Test de configuration email - Notytex Félicitations ! Votre configuration email fonctionne correctement. Vous pouvez maintenant envoyer des bilans d'évaluation par email. --- Email envoyé depuis Notytex - Système de gestion scolaire """ return self.send_email([to_email], subject, html_body, text_body) @staticmethod def validate_email_addresses(emails: List[str]) -> Dict[str, Any]: """ Valide une liste d'adresses email. Args: emails: Liste des adresses à valider Returns: Dict avec les emails valides et invalides """ email_regex = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') valid_emails = [] invalid_emails = [] for email in emails: email = email.strip() if email and email_regex.match(email): valid_emails.append(email) elif email: # Email non vide mais invalide invalid_emails.append(email) return { 'valid': valid_emails, 'invalid': invalid_emails, 'valid_count': len(valid_emails), 'invalid_count': len(invalid_emails) } @classmethod def from_database_config(cls, db_config: Dict[str, Any]) -> "EmailService": """ Crée une instance à partir d'une configuration de base de données. Args: db_config: Configuration extraite de la DB (format key-value) Returns: Instance d'EmailService configurée """ smtp_config = SMTPConfig( host=db_config.get('email.smtp_host', ''), port=int(db_config.get('email.smtp_port', 587)), username=db_config.get('email.username', ''), password=db_config.get('email.password', ''), use_tls=str(db_config.get('email.use_tls', 'true')).lower() == 'true', from_name=db_config.get('email.from_name', 'Notytex'), from_address=db_config.get('email.from_address', ''), ) return cls(smtp_config)