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:
@@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user