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 json
import logging import logging
import smtplib import smtplib
import threading
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
@@ -11,6 +13,9 @@ from backend.config import charger_config, RACINE
log = logging.getLogger("photobooth.mailer") 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: def envoyer_photo(destinataire: str, chemin_photo: Path) -> bool:
"""Envoie une photo par email.""" """Envoie une photo par email."""
@@ -66,6 +71,80 @@ def envoyer_photo(destinataire: str, chemin_photo: Path) -> bool:
return False 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" FICHIER_EMAILS = RACINE / "data" / "emails_history.json"

View File

@@ -28,7 +28,7 @@ from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lis
from backend.collage import creer_strip, creer_collage from backend.collage import creer_strip, creer_collage
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password
from backend.printer import lister_imprimantes, imprimer from backend.printer import lister_imprimantes, imprimer
from backend.mailer import envoyer_photo, charger_emails_historique, effacer_emails_historique from backend.mailer import envoyer_photo, charger_emails_historique, effacer_emails_historique, ajouter_au_spool, taille_spool, tache_spool_periodique
from backend.qrcode_gen import generer_qr, qr_galerie from backend.qrcode_gen import generer_qr, qr_galerie
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
@@ -187,9 +187,11 @@ async def lifespan(app: FastAPI):
t.start() t.start()
task_dslr = asyncio.create_task(surveiller_dslr()) task_dslr = asyncio.create_task(surveiller_dslr())
task_push = asyncio.create_task(_pusher_preview()) task_push = asyncio.create_task(_pusher_preview())
task_spool = asyncio.create_task(tache_spool_periodique())
yield yield
task_dslr.cancel() task_dslr.cancel()
task_push.cancel() task_push.cancel()
task_spool.cancel()
log.info("Arret du photobooth") log.info("Arret du photobooth")
camera.deconnecter() camera.deconnecter()
@@ -774,7 +776,11 @@ async def api_email(donnees: dict):
if not chemin.exists(): if not chemin.exists():
return JSONResponse({"erreur": "Photo introuvable"}, status_code=404) return JSONResponse({"erreur": "Photo introuvable"}, status_code=404)
ok = envoyer_photo(email_dest, chemin) ok = envoyer_photo(email_dest, chemin)
return {"succes": ok} if ok:
return {"succes": True}
ajouter_au_spool(email_dest, str(chemin))
_sauvegarder_email_historique(email_dest)
return {"succes": True, "spool": True, "en_attente": taille_spool()}
@app.get("/api/emails/historique") @app.get("/api/emails/historique")

View File

@@ -142,8 +142,10 @@ async function envoyerEmail() {
fermerEmail(); fermerEmail();
afficherStatut('Envoi en cours...', 'succes'); afficherStatut('Envoi en cours...', 'succes');
const resultat = await apiPost('/api/email', { email, photo: photoFinale }); const resultat = await apiPost('/api/email', { email, photo: photoFinale });
if (resultat.succes) { if (resultat.spool) {
afficherStatut('Email envoye !', 'succes'); afficherStatut(`Email en file d'attente (${resultat.en_attente}) — sera envoyé au retour de la connexion`, 'succes');
} else if (resultat.succes) {
afficherStatut('Email envoyé !', 'succes');
} else { } else {
afficherStatut('Erreur d\'envoi email', 'erreur'); afficherStatut('Erreur d\'envoi email', 'erreur');
} }