Email spool : file d'attente quand envoi echoue, retry toutes les 60s

- Si l'envoi SMTP echoue (pas de connexion, config incomplete, etc.),
  l'email est stocke dans data/email_spool.json
- Tache asyncio periodique (60s) tente de vider le spool
- L'adresse est quand meme ajoutee a l'historique de suggestions
- Frontend affiche "Email en file d'attente" au lieu de "Erreur"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 19:17:58 +02:00
parent 158f1b509f
commit e47ff04933
3 changed files with 91 additions and 4 deletions

View File

@@ -1,6 +1,8 @@
import asyncio
import json
import logging
import smtplib
import threading
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
@@ -11,6 +13,9 @@ from backend.config import charger_config, RACINE
log = logging.getLogger("photobooth.mailer")
FICHIER_SPOOL = RACINE / "data" / "email_spool.json"
_spool_lock = threading.Lock()
def envoyer_photo(destinataire: str, chemin_photo: Path) -> bool:
"""Envoie une photo par email."""
@@ -66,6 +71,80 @@ def envoyer_photo(destinataire: str, chemin_photo: Path) -> bool:
return False
# --- Spool : file d'attente pour emails en echec ---
def ajouter_au_spool(destinataire: str, chemin_photo: str):
with _spool_lock:
spool = _charger_spool()
spool.append({"email": destinataire, "photo": chemin_photo})
_sauver_spool(spool)
log.info(f"Email pour {destinataire} mis en file d'attente ({len(spool)} en attente)")
def _charger_spool() -> list:
if not FICHIER_SPOOL.exists():
return []
try:
with open(FICHIER_SPOOL, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return []
def _sauver_spool(spool: list):
try:
with open(FICHIER_SPOOL, "w", encoding="utf-8") as f:
json.dump(spool, f, ensure_ascii=False)
except OSError:
pass
def taille_spool() -> int:
with _spool_lock:
return len(_charger_spool())
def traiter_spool() -> int:
"""Tente d'envoyer les emails en attente. Retourne le nombre envoyes."""
with _spool_lock:
spool = _charger_spool()
if not spool:
return 0
envoyes = 0
restants = []
for entry in spool:
chemin = Path(entry["photo"])
if not chemin.exists():
log.warning(f"Spool: photo introuvable {chemin}, email ignore")
continue
ok = envoyer_photo(entry["email"], chemin)
if ok:
envoyes += 1
else:
restants.append(entry)
break # si un echoue, inutile de continuer (pas de connexion)
if envoyes > 0 or len(restants) < len(spool):
# garder les non-envoyes + ceux pas encore tentes
idx = envoyes + len(restants)
restants.extend(spool[idx:])
with _spool_lock:
_sauver_spool(restants)
log.info(f"Spool: {envoyes} envoye(s), {len(restants)} en attente")
return envoyes
async def tache_spool_periodique():
"""Tache asyncio qui vide le spool toutes les 60s."""
while True:
await asyncio.sleep(60)
if taille_spool() > 0:
log.info(f"Spool: tentative d'envoi ({taille_spool()} en attente)")
await asyncio.get_event_loop().run_in_executor(None, traiter_spool)
FICHIER_EMAILS = RACINE / "data" / "emails_history.json"