Files
notytex/backend/infrastructure/external/email_service.py
Bertrand Benjamin 2b08eb534a Migration v1 (Flask) -> v2 (FastAPI + Vue.js) complétée
 Changements majeurs:
- Suppression complète du code Flask legacy
- Migration backend FastAPI vers racine /backend
- Migration frontend Vue.js vers racine /frontend
- Suppression de notytex-v2/ (code monté à la racine)

 Validations:
- Backend démarre correctement (port 8000)
- API /api/v2/health répond healthy
- 99/99 tests unitaires passent
- Frontend configuré avec proxy Vite

📝 Documentation:
- README.md réécrit pour v2
- Instructions de démarrage mises à jour
- .gitignore adapté pour backend/frontend/

🎯 Architecture finale:
notytex/
├── backend/     # FastAPI + SQLAlchemy + Pydantic
├── frontend/    # Vue 3 + Vite + TailwindCSS
├── docs/        # Documentation
└── school_management.db  # Base de données (inchangée)

Jalon 6 complété: Application v2 prête pour utilisation!
2025-11-25 21:09:47 +01:00

247 lines
8.1 KiB
Python

"""
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 = """
<html>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h2 style="color: #3b82f6;">Test de configuration email</h2>
<p>Félicitations ! Votre configuration email fonctionne correctement.</p>
<p>Vous pouvez maintenant envoyer des bilans d'évaluation par email.</p>
<hr style="margin: 20px 0;">
<p style="color: #6b7280; font-size: 12px;">
Email envoyé depuis Notytex - Système de gestion scolaire
</p>
</body>
</html>
"""
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)