Remise en état des alertes tables
This commit is contained in:
4
.env
4
.env
@@ -34,11 +34,11 @@ LOGLEVEL=INFO
|
|||||||
# paramètres mail
|
# paramètres mail
|
||||||
SMTP_HOST=ssl0.ovh.net
|
SMTP_HOST=ssl0.ovh.net
|
||||||
SMTP_PORT=465
|
SMTP_PORT=465
|
||||||
SMTP_SECURITY=STARTTLS
|
SMTP_SECURITY=SSL
|
||||||
SMTP_USER=services@domo91.fr
|
SMTP_USER=services@domo91.fr
|
||||||
SMTP_PASS=VHq3278YA#sGV*bh#mR
|
SMTP_PASS=VHq3278YA#sGV*bh#mR
|
||||||
MAIL_FROM=services@domo91.fr
|
MAIL_FROM=services@domo91.fr
|
||||||
MAIL_TO=michel@mj91.fr
|
MAIL_TO=robots@domo91.fr
|
||||||
MAIL_TO_SACLAY=robots@domo91.fr,nicolas.thibaut@bw-paris-saclay.com
|
MAIL_TO_SACLAY=robots@domo91.fr,nicolas.thibaut@bw-paris-saclay.com
|
||||||
MAIL_FROM_SACLAY="DOMO91 Saclay <services@domo91.fr>"
|
MAIL_FROM_SACLAY="DOMO91 Saclay <services@domo91.fr>"
|
||||||
MAIL_TO_MEUDON=robots@domo91.fr,chef@parismeudonermitage.com
|
MAIL_TO_MEUDON=robots@domo91.fr,chef@parismeudonermitage.com
|
||||||
|
|||||||
@@ -865,9 +865,9 @@ if st.session_state.get("authenticated"):
|
|||||||
)
|
)
|
||||||
st.session_state["selected_site"] = site_actuel
|
st.session_state["selected_site"] = site_actuel
|
||||||
else:
|
else:
|
||||||
st.info(f"Site imposé : {site_actuel}")
|
st.info(f"Site imposé")
|
||||||
|
#st.info(f"Site imposé : {site_actuel}")
|
||||||
assert_site_ok(site_actuel)
|
#assert_site_ok(site_actuel)
|
||||||
|
|
||||||
# Voyant Gyro
|
# Voyant Gyro
|
||||||
st.subheader(f"🚨 Statut Gyro — {site_actuel}")
|
st.subheader(f"🚨 Statut Gyro — {site_actuel}")
|
||||||
|
|||||||
@@ -1,95 +1,113 @@
|
|||||||
#!/home/debian/Gestion_sondes/myenv/bin/python3
|
#!/usr/bin/env python3
|
||||||
# Surveillance de l'arrivée des relevés (par table/site) + SMS d'alerte / retour à la normale
|
# Surveillance des réceptions de données dans les tables (par site) + mail d'alerte / retour à la normale
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dotenv import load_dotenv
|
|
||||||
import os
|
import os
|
||||||
import logging
|
|
||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
from contextlib import closing
|
||||||
import mysql.connector # important pour cibler les exceptions MySQL
|
import mysql.connector # important pour cibler les exceptions MySQL
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
import utils_db
|
import utils_db
|
||||||
from utils_mail import envoyer_mail
|
from utils_mail import envoyer_mail
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(
|
# -------------------- PATHS (portable Windows/Linux) --------------------
|
||||||
level=logging.INFO,
|
APP_DIR = os.path.dirname(os.path.abspath(__file__)) # .../Gestion_sondes/app
|
||||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
ROOT_DIR = os.path.abspath(os.path.join(APP_DIR, "..")) # .../Gestion_sondes
|
||||||
handlers=[logging.StreamHandler(sys.stdout)]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Forcer l'encodage UTF-8 du flux si possible (Windows/PyCharm)
|
ENV_PATH = os.path.join(ROOT_DIR, ".env")
|
||||||
try:
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
LOG_DIR = os.path.join(ROOT_DIR, "Logs")
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# -------------------- LOGS --------------------
|
|
||||||
LOG_DIR = '/home/debian/Gestion_sondes/Logs'
|
|
||||||
os.makedirs(LOG_DIR, exist_ok=True)
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
log_filename = os.path.join(LOG_DIR, datetime.now().strftime("surveillance_%Y-%m-%d.log"))
|
log_filename = os.path.join(LOG_DIR, datetime.now().strftime("surveillance_%Y-%m-%d.log"))
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------- LOGGING (UTF-8 + fichier + console) --------------------
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
handlers=[logging.FileHandler(log_filename), logging.StreamHandler()]
|
handlers=[
|
||||||
|
logging.FileHandler(log_filename, encoding="utf-8"),
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
],
|
||||||
|
force=True, # Python 3.8+
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Fix Windows/PyCharm : forcer l'encodage UTF-8 sur le StreamHandler (évite UnicodeEncodeError cp1252)
|
||||||
|
for h in logging.getLogger().handlers:
|
||||||
|
if isinstance(h, logging.StreamHandler):
|
||||||
|
try:
|
||||||
|
h.stream.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
# Certains streams/IDEs ne supportent pas reconfigure
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# -------------------- ENV --------------------
|
# -------------------- ENV --------------------
|
||||||
load_dotenv('/home/debian/Gestion_sondes/.env')
|
# override=True : si cron/supervisor/IDE a des variables vides, le .env reprend la main
|
||||||
|
load_dotenv(ENV_PATH, override=True)
|
||||||
|
|
||||||
|
|
||||||
# -------------------- PARAMETRES --------------------
|
# -------------------- PARAMETRES --------------------
|
||||||
tables = ['Saclay', 'Meudon']
|
# Mets ici les tables réelles à surveiller (noms exacts des tables MySQL)
|
||||||
|
tables = ["Saclay", "Meudon"]
|
||||||
|
|
||||||
DELAI_MINUTES = 15
|
DELAI_MINUTES = 15
|
||||||
RAPPEL_HEURES = 6
|
RAPPEL_HEURES = 6
|
||||||
|
|
||||||
STATE_DIR = '/tmp/surveillance_states'
|
# State portable (Windows/Linux)
|
||||||
|
STATE_DIR = os.path.join(tempfile.gettempdir(), "surveillance_states")
|
||||||
os.makedirs(STATE_DIR, exist_ok=True)
|
os.makedirs(STATE_DIR, exist_ok=True)
|
||||||
|
|
||||||
TABLES_SET = set(tables) # whitelist simple
|
TABLES_SET = set(tables) # whitelist simple
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------- STATE MANAGEMENT --------------------
|
||||||
def _state_file(site: str) -> str:
|
def _state_file(site: str) -> str:
|
||||||
return os.path.join(STATE_DIR, f'{site}.state')
|
return os.path.join(STATE_DIR, f"{site}.state")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_last_alert(site: str) -> datetime | None:
|
||||||
|
sf = _state_file(site)
|
||||||
|
if not os.path.exists(sf):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(sf, "r", encoding="utf-8") as f:
|
||||||
|
raw = f.read().strip()
|
||||||
|
return datetime.fromisoformat(raw)
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
logging.warning(f"Etat corrompu pour {site} ({sf}) : {e}. On ignorera cet état.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def mark_alert_sent(site: str, when: datetime | None = None) -> None:
|
||||||
|
"""Écrit/actualise l'état *après* un envoi mail réussi."""
|
||||||
|
sf = _state_file(site)
|
||||||
|
when = when or datetime.now()
|
||||||
|
try:
|
||||||
|
with open(sf, "w", encoding="utf-8") as f:
|
||||||
|
f.write(when.isoformat())
|
||||||
|
except OSError as e:
|
||||||
|
logging.warning(f"Impossible d'écrire l'état {sf} : {e} (l'anti-spam sera moins efficace).")
|
||||||
|
|
||||||
|
|
||||||
def should_send_alert(site: str) -> bool:
|
def should_send_alert(site: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Retourne True si on doit envoyer une alerte maintenant (première fois ou rappel).
|
Retourne True si on doit envoyer une alerte maintenant (première fois ou rappel).
|
||||||
Met à jour le fichier d'état en cas d'envoi.
|
IMPORTANT : ne modifie PAS l'état ici.
|
||||||
|
L'état est mis à jour uniquement après envoi réussi (mark_alert_sent).
|
||||||
"""
|
"""
|
||||||
sf = _state_file(site)
|
last_alert = _read_last_alert(site)
|
||||||
now = datetime.now()
|
if last_alert is None:
|
||||||
|
|
||||||
if not os.path.exists(sf):
|
|
||||||
# première alerte
|
|
||||||
try:
|
|
||||||
with open(sf, 'w') as f:
|
|
||||||
f.write(now.isoformat())
|
|
||||||
except OSError as e:
|
|
||||||
logging.warning(f"Impossible d'écrire l'état {sf} : {e} (on alerte quand même).")
|
|
||||||
return True
|
return True
|
||||||
|
return (datetime.now() - last_alert) >= timedelta(hours=RAPPEL_HEURES)
|
||||||
|
|
||||||
try:
|
|
||||||
with open(sf, 'r') as f:
|
|
||||||
raw = f.read().strip()
|
|
||||||
last_alert = datetime.fromisoformat(raw)
|
|
||||||
except (OSError, ValueError) as e:
|
|
||||||
# état illisible/corrompu -> on réinitialise et on alerte
|
|
||||||
logging.warning(f"Etat corrompu pour {site} ({sf}) : {e}. Réinitialisation.")
|
|
||||||
try:
|
|
||||||
with open(sf, 'w') as f:
|
|
||||||
f.write(now.isoformat())
|
|
||||||
except OSError as e2:
|
|
||||||
logging.warning(f"Impossible de réécrire l'état {sf} : {e2}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
if now - last_alert >= timedelta(hours=RAPPEL_HEURES):
|
|
||||||
try:
|
|
||||||
with open(sf, 'w') as f:
|
|
||||||
f.write(now.isoformat())
|
|
||||||
except OSError as e:
|
|
||||||
logging.warning(f"Impossible de mettre à jour l'état {sf} : {e} (on alerte quand même).")
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def clear_state(site: str) -> None:
|
def clear_state(site: str) -> None:
|
||||||
sf = _state_file(site)
|
sf = _state_file(site)
|
||||||
@@ -99,80 +117,111 @@ def clear_state(site: str) -> None:
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
logging.warning(f"Impossible de supprimer l'état {sf} : {e}")
|
logging.warning(f"Impossible de supprimer l'état {sf} : {e}")
|
||||||
|
|
||||||
def main():
|
|
||||||
problemes = []
|
# -------------------- MAIN --------------------
|
||||||
|
def main() -> None:
|
||||||
limite = datetime.now() - timedelta(minutes=DELAI_MINUTES)
|
limite = datetime.now() - timedelta(minutes=DELAI_MINUTES)
|
||||||
|
|
||||||
|
# pour logs plus clairs
|
||||||
|
defauts_en_cours: list[str] = [] # défaut détecté (même si déjà signalé)
|
||||||
|
alertes_envoyees: list[str] = [] # alertes envoyées à CE run (pour mail groupé)
|
||||||
|
erreurs_sql: list[str] = [] # SQL KO sur tables
|
||||||
|
|
||||||
# 1) Connexion MySQL (une seule fois)
|
# 1) Connexion MySQL (une seule fois)
|
||||||
try:
|
try:
|
||||||
cnx = utils_db.connect_to_mysql()
|
with closing(utils_db.connect_to_mysql()) as cnx, closing(cnx.cursor()) as cursor:
|
||||||
cursor = cnx.cursor()
|
|
||||||
except mysql.connector.Error as e:
|
|
||||||
logging.error(f"MySQL KO : {e}")
|
|
||||||
envoyer_mail(
|
|
||||||
"⚠️ ALERTE : Base MySQL inaccessible (surveillance impossible).",
|
|
||||||
"Connexion MySQL impossible : la surveillance des relevés ne peut pas s’exécuter."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2) Surveillance par table (try SQL à l'intérieur de la boucle)
|
# 2) Surveillance par table
|
||||||
try:
|
|
||||||
for table in tables:
|
for table in tables:
|
||||||
if table not in TABLES_SET:
|
if table not in TABLES_SET:
|
||||||
logging.warning(f"Table ignorée (non whitelistée) : {table}")
|
logging.warning(f"Table ignorée (non whitelistée) : {table}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 2a) Lecture de la dernière date (erreurs SQL gérées finement)
|
# 2a) Lecture de la dernière date
|
||||||
try:
|
try:
|
||||||
cursor.execute(f"SELECT MAX(Date) FROM `{table}`")
|
cursor.execute(f"SELECT MAX(Date) FROM `{table}`")
|
||||||
(last_update,) = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
|
last_update = row[0] if row else None
|
||||||
except mysql.connector.Error as e:
|
except mysql.connector.Error as e:
|
||||||
logging.error(f"Erreur SQL sur {table} : {e}")
|
msg = f"Erreur SQL sur {table} : {e}"
|
||||||
# Vous pouvez décider ici si vous voulez un SMS ou seulement un log.
|
erreurs_sql.append(table)
|
||||||
|
logging.error(msg)
|
||||||
|
|
||||||
|
# Alerte SQL (anti-spam)
|
||||||
if should_send_alert(table):
|
if should_send_alert(table):
|
||||||
|
try:
|
||||||
envoyer_mail(
|
envoyer_mail(
|
||||||
f"⚠️ ALERTE : erreur SQL sur {table} (voir logs).",
|
f"⚠️ ALERTE : erreur SQL sur {table} (voir logs).",
|
||||||
f"Erreur SQL détectée sur la table {table}. Merci de consulter le fichier log pour le détail."
|
f"Erreur SQL détectée sur la table {table}.\n\nDétail:\n{e}"
|
||||||
)
|
)
|
||||||
|
mark_alert_sent(table)
|
||||||
|
alertes_envoyees.append(f"{table} (SQL)")
|
||||||
|
except Exception as mail_e:
|
||||||
|
logging.error(f"Impossible d'envoyer le mail d'erreur SQL pour {table} : {mail_e}")
|
||||||
|
else:
|
||||||
|
logging.info(f"⏳ {table} : erreur SQL déjà signalée, rappel dans {RAPPEL_HEURES}h.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 2b) Logique métier (hors try SQL)
|
# 2b) Logique métier
|
||||||
if (last_update is None) or (last_update < limite):
|
if (last_update is None) or (last_update < limite):
|
||||||
|
defauts_en_cours.append(table)
|
||||||
|
|
||||||
if should_send_alert(table):
|
if should_send_alert(table):
|
||||||
problemes.append(f"{table} (dernier relevé : {last_update})")
|
|
||||||
logging.warning(f"⚠️ {table} en défaut (dernier relevé : {last_update})")
|
logging.warning(f"⚠️ {table} en défaut (dernier relevé : {last_update})")
|
||||||
|
|
||||||
|
# On envoie le mail tout de suite (alerte individuelle)
|
||||||
|
try:
|
||||||
|
envoyer_mail(
|
||||||
|
f"⚠️ ALERTE : {table} absence de relevés",
|
||||||
|
f"Pas de relevés depuis > {DELAI_MINUTES} min.\nDernier relevé : {last_update}"
|
||||||
|
)
|
||||||
|
mark_alert_sent(table)
|
||||||
|
alertes_envoyees.append(f"{table} (dernier : {last_update})")
|
||||||
|
except Exception as mail_e:
|
||||||
|
# pas de mark_alert_sent => on retentera au prochain run
|
||||||
|
logging.error(f"Erreur envoi mail alerte pour {table} : {mail_e}")
|
||||||
else:
|
else:
|
||||||
logging.info(f"⏳ {table} déjà signalé, rappel dans {RAPPEL_HEURES}h.")
|
logging.info(f"⏳ {table} déjà signalé, rappel dans {RAPPEL_HEURES}h.")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Retour à la normale uniquement si on était en défaut (state présent)
|
# OK => si on avait un état, on envoie un "retour à la normale"
|
||||||
if os.path.exists(_state_file(table)):
|
if os.path.exists(_state_file(table)):
|
||||||
message = f"✅ {table} : relevés à nouveau reçus (dernier : {last_update}). Situation normale."
|
message = f"✅ {table} : relevés à nouveau reçus (dernier : {last_update}). Situation normale."
|
||||||
envoyer_mail(
|
try:
|
||||||
f"✅ OK : {table} relevés reçus",
|
envoyer_mail(f"✅ OK : {table} relevés reçus", message)
|
||||||
message
|
|
||||||
)
|
|
||||||
clear_state(table)
|
clear_state(table)
|
||||||
logging.info(f"📩 Retour à la normale envoyé pour {table}.")
|
logging.info(f"📩 Retour à la normale envoyé pour {table}.")
|
||||||
|
except Exception as mail_e:
|
||||||
|
# on conserve le state => on retentera le retour à la normale
|
||||||
|
logging.error(f"Erreur envoi mail retour à la normale pour {table} : {mail_e}")
|
||||||
else:
|
else:
|
||||||
logging.info(f"✅ {table} OK (dernier relevé : {last_update})")
|
logging.info(f"✅ {table} OK (dernier relevé : {last_update})")
|
||||||
|
|
||||||
finally:
|
except mysql.connector.Error as e:
|
||||||
# 3) Nettoyage MySQL
|
logging.error(f"MySQL KO : {e}")
|
||||||
try:
|
try:
|
||||||
cursor.close()
|
|
||||||
cnx.close()
|
|
||||||
except mysql.connector.Error:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4) Alerte groupée si besoin
|
|
||||||
if problemes:
|
|
||||||
message = f"⚠️ ALERTE : pas de relevés depuis >{DELAI_MINUTES}min :\n" + "\n".join(problemes)
|
|
||||||
envoyer_mail(
|
envoyer_mail(
|
||||||
f"⚠️ ALERTE : absence de relevés > {DELAI_MINUTES} min",
|
"⚠️ ALERTE : Base MySQL inaccessible (surveillance impossible).",
|
||||||
message
|
"Connexion MySQL impossible : la surveillance des relevés ne peut pas s’exécuter."
|
||||||
)
|
)
|
||||||
|
except Exception as mail_e:
|
||||||
|
logging.error(f"Impossible d'envoyer le mail MySQL KO : {mail_e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3) Logs de synthèse (fidèles à la réalité)
|
||||||
|
if alertes_envoyees:
|
||||||
|
logging.info("📧 Mail(s) envoyé(s) : " + ", ".join(alertes_envoyees))
|
||||||
|
elif defauts_en_cours or erreurs_sql:
|
||||||
|
# défauts en cours mais déjà signalés (ou erreurs SQL déjà signalées)
|
||||||
|
bloc = []
|
||||||
|
if defauts_en_cours:
|
||||||
|
bloc.append("défaut(s) relevés: " + ", ".join(defauts_en_cours))
|
||||||
|
if erreurs_sql:
|
||||||
|
bloc.append("erreur(s) SQL: " + ", ".join(erreurs_sql))
|
||||||
|
logging.info("⚠️ " + " | ".join(bloc) + " (déjà signalé / pas de mail envoyé à ce run)")
|
||||||
else:
|
else:
|
||||||
logging.info("👍 Tout est OK, aucun Mail envoyé.")
|
logging.info("👍 Tout est OK, aucun Mail envoyé.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
# /home/debian/Gestion_sondes/utils_mail.py
|
|
||||||
import os
|
import os
|
||||||
import smtplib
|
import smtplib
|
||||||
import logging
|
import logging
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv('/home/debian/Gestion_sondes/.env')
|
# Charge le .env (et override pour éviter des variables vides héritées de cron/supervisor)
|
||||||
|
load_dotenv('/home/debian/Gestion_sondes/.env', override=True)
|
||||||
|
|
||||||
SMTP_HOST = os.getenv("SMTP_HOST", "smtp.mail.ovh.net")
|
SMTP_HOST = os.getenv("SMTP_HOST")
|
||||||
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
||||||
|
|
||||||
SMTP_LOGIN = os.getenv("SMTP_USER") # ex: services@domo91.fr
|
SMTP_LOGIN = os.getenv("SMTP_USER") # <-- correspond à ton .env
|
||||||
SMTP_PASSWORD = os.getenv("SMTP_PASS") # mot de passe OVH
|
SMTP_PASSWORD = os.getenv("SMTP_PASS") # <-- correspond à ton .env
|
||||||
MAIL_FROM = os.getenv("MAIL_FROM", SMTP_LOGIN)
|
|
||||||
MAIL_TO = os.getenv("MAIL_TO") # ex: services@domo91.fr
|
MAIL_FROM = os.getenv("MAIL_FROM")
|
||||||
|
MAIL_TO = os.getenv("MAIL_TO")
|
||||||
|
|
||||||
def envoyer_mail(sujet: str, contenu: str, destinataires=None) -> None:
|
def envoyer_mail(sujet: str, contenu: str, destinataires=None) -> None:
|
||||||
"""
|
|
||||||
Envoi email via OVH SMTP SSL 465 (process identique à supervisor_watchdog.py).
|
|
||||||
destinataires: str unique ou liste; si None => MAIL_TO depuis .env
|
|
||||||
"""
|
|
||||||
if destinataires is None:
|
if destinataires is None:
|
||||||
if not MAIL_TO:
|
if not MAIL_TO:
|
||||||
raise ValueError("MAIL_TO manquant dans le .env")
|
raise ValueError("MAIL_TO manquant dans le .env")
|
||||||
@@ -27,19 +24,21 @@ def envoyer_mail(sujet: str, contenu: str, destinataires=None) -> None:
|
|||||||
elif isinstance(destinataires, str):
|
elif isinstance(destinataires, str):
|
||||||
destinataires = [destinataires]
|
destinataires = [destinataires]
|
||||||
|
|
||||||
|
if not SMTP_HOST:
|
||||||
|
raise ValueError("SMTP_HOST manquant dans le .env")
|
||||||
if not SMTP_LOGIN or not SMTP_PASSWORD:
|
if not SMTP_LOGIN or not SMTP_PASSWORD:
|
||||||
raise ValueError("SMTP_LOGIN / SMTP_PASSWORD manquants dans le .env")
|
raise ValueError("SMTP_USER / SMTP_PASS manquants dans le .env")
|
||||||
|
if not MAIL_FROM:
|
||||||
|
raise ValueError("MAIL_FROM manquant dans le .env")
|
||||||
|
|
||||||
msg = MIMEText(contenu)
|
msg = MIMEText(contenu)
|
||||||
msg["Subject"] = sujet
|
msg["Subject"] = sujet
|
||||||
msg["From"] = MAIL_FROM
|
msg["From"] = MAIL_FROM
|
||||||
msg["To"] = ", ".join(destinataires)
|
msg["To"] = ", ".join(destinataires)
|
||||||
|
|
||||||
try:
|
# SSL direct (OVH ssl0.ovh.net:465)
|
||||||
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server:
|
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=30) as server:
|
||||||
server.login(SMTP_LOGIN, SMTP_PASSWORD)
|
server.login(SMTP_LOGIN, SMTP_PASSWORD)
|
||||||
server.sendmail(MAIL_FROM, destinataires, msg.as_string())
|
server.sendmail(MAIL_FROM, destinataires, msg.as_string())
|
||||||
|
|
||||||
logging.info("📧 Mail envoyé: %s -> %s", sujet, destinataires)
|
logging.info("📧 Mail envoyé: %s -> %s", sujet, destinataires)
|
||||||
except Exception as e:
|
|
||||||
logging.error("Erreur envoi mail: %s", e)
|
|
||||||
raise
|
|
||||||
|
|||||||
Reference in New Issue
Block a user