diff --git a/backend/mailer.py b/backend/mailer.py index fa62448..4f6113b 100644 --- a/backend/mailer.py +++ b/backend/mailer.py @@ -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" diff --git a/backend/main.py b/backend/main.py index f602665..eb3f69b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password 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 logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s") @@ -187,9 +187,11 @@ async def lifespan(app: FastAPI): t.start() task_dslr = asyncio.create_task(surveiller_dslr()) task_push = asyncio.create_task(_pusher_preview()) + task_spool = asyncio.create_task(tache_spool_periodique()) yield task_dslr.cancel() task_push.cancel() + task_spool.cancel() log.info("Arret du photobooth") camera.deconnecter() @@ -774,7 +776,11 @@ async def api_email(donnees: dict): if not chemin.exists(): return JSONResponse({"erreur": "Photo introuvable"}, status_code=404) 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") diff --git a/frontend/js/share.js b/frontend/js/share.js index 5ce04bb..1ddf563 100644 --- a/frontend/js/share.js +++ b/frontend/js/share.js @@ -142,8 +142,10 @@ async function envoyerEmail() { fermerEmail(); afficherStatut('Envoi en cours...', 'succes'); const resultat = await apiPost('/api/email', { email, photo: photoFinale }); - if (resultat.succes) { - afficherStatut('Email envoye !', 'succes'); + if (resultat.spool) { + 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 { afficherStatut('Erreur d\'envoi email', 'erreur'); }