- Service mail_service.py: send_html_mail via SMTP standard (host/port/user/pass/from/use_tls
depuis Settings > SMTP). Gere SSL_465 et STARTTLS_587. Mode dry_run pour preview.
- Settings: nouvelle section 'smtp' avec smtp_host/port/user/pass/from/use_tls/pct_recipient
(a configurer pour O365 SMTP submission)
- Router planning_import.py:
* _build_pct_email(): construit subject + HTML pro/colore (header bleu degrade SANEF,
cards avec border-left bleu/orange, tableau serveurs, footer)
* Subject: 'Intervention sur <app>' si app uniforme, sinon liste des serveurs
* Plage horaire = 20 min × N serveurs (formattee Hh MM)
* 'Moyen d'exploitation prevu : Rollback en cas de probleme' ajoute en bas
* _fetch_pct_cc_emails(): query distinct contacts depuis responsable_domaine_contact_id
+ referent_technique_contact_id + server_additional_referents
* Endpoint POST /patching/import/pct-prevenance/preview retourne {subject, html, to, cc,
smtp_configured, row_count} sans envoyer
* Endpoint POST /patching/import/pct-prevenance/send envoie reellement, audit log,
update pct_mail_sent_at sur les rows
- Template patching_import.html:
* Bouton 'Prevenance PCT' (violet) a cote des autres actions
* Modal preview avec iframe sandboxe pour le rendu HTML mail
* Affiche destinataires, CC, objet, count serveurs
* Warning rouge si SMTP non configure (envoi desactive, preview seulement)
* 2 boutons: Annuler / Envoyer (avec confirmation)
104 lines
3.8 KiB
Python
104 lines
3.8 KiB
Python
"""Service mail simple — envoi HTML via SMTP (Office 365 ou autre relay).
|
|
|
|
Configuration via Settings > SMTP :
|
|
- smtp_host, smtp_port, smtp_user, smtp_pass, smtp_from, smtp_use_tls
|
|
|
|
Si SMTP non configuré, send_html_mail retourne {ok:False, msg, html_preview}
|
|
pour que l'UI affiche au moins le rendu sans envoi.
|
|
"""
|
|
import logging
|
|
import smtplib
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
from typing import Iterable
|
|
|
|
log = logging.getLogger("patchcenter.mail")
|
|
|
|
|
|
def _get(db, key, default=""):
|
|
try:
|
|
from .secrets_service import get_secret
|
|
return (get_secret(db, key) or default).strip()
|
|
except Exception:
|
|
return default
|
|
|
|
|
|
def get_smtp_config(db):
|
|
"""Lit la config SMTP depuis app_secrets. Retourne dict ou None si incomplet."""
|
|
host = _get(db, "smtp_host")
|
|
port_str = _get(db, "smtp_port", "587")
|
|
sender = _get(db, "smtp_from")
|
|
user = _get(db, "smtp_user")
|
|
pwd = _get(db, "smtp_pass")
|
|
use_tls = _get(db, "smtp_use_tls", "true").lower() in ("true", "1", "yes", "on")
|
|
if not host or not sender:
|
|
return None
|
|
try:
|
|
port = int(port_str or "587")
|
|
except ValueError:
|
|
port = 587
|
|
return {
|
|
"host": host, "port": port, "from": sender,
|
|
"user": user, "pass": pwd, "use_tls": use_tls,
|
|
}
|
|
|
|
|
|
def send_html_mail(db, to: Iterable[str], subject: str, html: str,
|
|
cc: Iterable[str] = None, dry_run: bool = False) -> dict:
|
|
"""Envoie un mail HTML.
|
|
- to : liste d'adresses (ou string seul)
|
|
- dry_run : si True, ne tente pas l'envoi (juste renvoie le rendu)
|
|
Retourne {ok, msg, sent_to, html_preview, subject_preview, error_kind}.
|
|
"""
|
|
if isinstance(to, str):
|
|
to = [to]
|
|
to = [a.strip() for a in to if a and a.strip()]
|
|
if not to:
|
|
return {"ok": False, "msg": "Destinataire(s) vide(s)"}
|
|
|
|
cfg = get_smtp_config(db)
|
|
if not cfg:
|
|
return {"ok": False, "msg": "Config SMTP incomplète (Settings > SMTP)",
|
|
"error_kind": "config_missing",
|
|
"html_preview": html, "subject_preview": subject, "sent_to": to}
|
|
|
|
if dry_run:
|
|
return {"ok": True, "msg": "Dry-run — pas d'envoi",
|
|
"html_preview": html, "subject_preview": subject, "sent_to": to}
|
|
|
|
msg = MIMEMultipart("alternative")
|
|
msg["Subject"] = subject
|
|
msg["From"] = cfg["from"]
|
|
msg["To"] = ", ".join(to)
|
|
if cc:
|
|
cc = [a.strip() for a in cc if a and a.strip()]
|
|
if cc:
|
|
msg["Cc"] = ", ".join(cc)
|
|
msg.attach(MIMEText(html, "html", "utf-8"))
|
|
|
|
recipients = list(to) + (list(cc) if cc else [])
|
|
try:
|
|
if cfg["port"] == 465:
|
|
with smtplib.SMTP_SSL(cfg["host"], cfg["port"], timeout=30) as srv:
|
|
if cfg["user"] and cfg["pass"]:
|
|
srv.login(cfg["user"], cfg["pass"])
|
|
srv.sendmail(cfg["from"], recipients, msg.as_string())
|
|
else:
|
|
with smtplib.SMTP(cfg["host"], cfg["port"], timeout=30) as srv:
|
|
srv.ehlo()
|
|
if cfg["use_tls"]:
|
|
srv.starttls()
|
|
srv.ehlo()
|
|
if cfg["user"] and cfg["pass"]:
|
|
srv.login(cfg["user"], cfg["pass"])
|
|
srv.sendmail(cfg["from"], recipients, msg.as_string())
|
|
log.info(f"Mail envoyé à {to} (subject={subject!r})")
|
|
return {"ok": True, "msg": f"Envoyé à {', '.join(to)}",
|
|
"sent_to": to, "subject_preview": subject}
|
|
except smtplib.SMTPAuthenticationError as e:
|
|
return {"ok": False, "msg": f"Auth SMTP refusée: {e}", "error_kind": "auth"}
|
|
except smtplib.SMTPException as e:
|
|
return {"ok": False, "msg": f"Erreur SMTP: {e}", "error_kind": "smtp"}
|
|
except Exception as e:
|
|
return {"ok": False, "msg": f"{type(e).__name__}: {e}", "error_kind": "other"}
|