"""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 via le backend configuré (Settings > SMTP > mail_backend).
Backends : 'smtp' (défaut, serveur) | 'outlook_com' (Windows local via Outlook).
"""
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)"}
backend = (_get(db, "mail_backend", "smtp") or "smtp").lower()
# ─── Backend Outlook COM (Windows local) ─────────────────────
if backend == "outlook_com":
if dry_run:
return {"ok": True, "msg": "Dry-run — pas d'envoi",
"html_preview": html, "subject_preview": subject, "sent_to": to,
"backend": "outlook_com"}
display_only = _get(db, "mail_outlook_display_only", "false").lower() in ("true", "1", "yes")
from .mail_outlook_com import send_via_outlook_com
return send_via_outlook_com(to, subject, html, cc=cc, display_only=display_only)
# ─── Backend SMTP (défaut, serveur) ───────────────────────────
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,
"backend": "smtp"}
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"}