"""Envoi d'emails SMTP et utilitaires de template (Office 365 / Brevo / Gmail).""" from __future__ import annotations import re import smtplib from datetime import datetime from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import TYPE_CHECKING if TYPE_CHECKING: from src.db import Absence, Apprenti def parse_sender(sender: str) -> tuple[str, str]: """Extrait (nom, email) depuis 'Nom Affiché ' ou 'email'.""" m = re.match(r'^(.+?)\s*<([^>]+)>\s*$', sender.strip()) if m: return m.group(1).strip(), m.group(2).strip() return "", sender.strip() def render_template(template: str, variables: dict) -> str: """Substitue les variables {prenom}, {nom}, etc. dans le template.""" try: return template.format_map(variables) except KeyError: return template def build_template_vars( apprenti: "Apprenti", absences: list["Absence"], semestre_label: str = "", ) -> dict: """Construit le dictionnaire de variables pour les templates email.""" nb_total = len(absences) nb_excusees = sum(1 for a in absences if a.statut == "excusee") nb_non_exc = sum(1 for a in absences if a.statut == "non_excusee") nb_a_traiter = sum(1 for a in absences if a.statut == "a_traiter") return { "prenom": apprenti.prenom, "nom": apprenti.nom, "nom_complet": f"{apprenti.nom} {apprenti.prenom}", "classe": apprenti.classe, "nb_absences": nb_total, "nb_excusees": nb_excusees, "nb_non_excusees": nb_non_exc, "nb_a_traiter": nb_a_traiter, "semestre": semestre_label or "", "date_du_jour": datetime.now().strftime("%d.%m.%Y"), } def send_email( smtp_host: str, smtp_port: int, smtp_login: str, smtp_password: str, smtp_sender: str, to_email: str, subject: str, body: str, attachments: "list[tuple[bytes, str]] | None" = None, ) -> None: """Envoie un email avec pièces jointes PDF optionnelles. smtp_login : identifiant d'authentification SMTP (peut différer de l'expéditeur). smtp_sender : adresse expéditeur, format 'Nom ' ou 'email'. attachments : liste de (pdf_bytes, filename). Lève une exception en cas d'échec (SMTPException, OSError). """ _from_name, _from_email = parse_sender(smtp_sender) msg = MIMEMultipart() msg["From"] = f"{_from_name} <{_from_email}>" if _from_name else _from_email msg["To"] = to_email msg["Subject"] = subject msg.attach(MIMEText(body, "plain", "utf-8")) for pdf_bytes, pdf_filename in (attachments or []): part = MIMEApplication(pdf_bytes, _subtype="pdf") part.add_header("Content-Disposition", "attachment", filename=pdf_filename) msg.attach(part) with smtplib.SMTP(smtp_host, smtp_port, timeout=20) as smtp: smtp.ehlo() smtp.starttls() smtp.ehlo() smtp.login(smtp_login, smtp_password) smtp.sendmail(_from_email, [to_email], msg.as_string())