✨ 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!
247 lines
8.1 KiB
Python
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)
|