Resout le probleme de firewall SMTP corporate qui bloque outbound 25/465/587 depuis les postes de travail. Outlook utilise HTTPS (EWS/Graph) vers O365 -> aucun firewall. Service mail_outlook_com.py: - send_via_outlook_com(to, subject, html, cc, display_only): pilote Outlook via COM pywin32. Mail apparait dans Sent Items du user, traçable. - Mode display_only=True ouvre la fenetre de composition Outlook au lieu d'envoyer automatiquement (relecture manuelle). - pythoncom.CoInitialize() pour fonctionner dans un thread uvicorn. Service mail_service.py: - send_html_mail dispatche selon Setting 'mail_backend' (smtp / outlook_com). - Defaut = smtp. Settings: - 'mail_backend' = 'smtp' | 'outlook_com' - 'mail_outlook_display_only' = 'true' | 'false' - UI: 2 selects en haut de la card SMTP avec hints + le SMTP existant en dessous Pre-requis Windows: pip install pywin32 + Outlook lance + connecte au compte O365.
116 lines
4.6 KiB
Python
116 lines
4.6 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 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"}
|