- Reorganisation menu admin en 2 rubriques (General / Evenement) - Destinations deplace dans Evenement - Nouveau panneau Videos : upload par situation (montage, depannage, bourrage, rechargement) - Nouveau panneau Surprise : photo/video affichee ~1s avant capture pour provoquer sourire - Toggle rembobinage ruban Mitsubishi (StpiDecklist) pour economiser le ruban sur petits formats - Historique email cloisonne par evenement - Memoire projet consolidee dans memoire.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
7.5 KiB
Python
242 lines
7.5 KiB
Python
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
|
|
from email import encoders
|
|
from pathlib import Path
|
|
|
|
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."""
|
|
config = charger_config()
|
|
conf_email = config.get("email", {})
|
|
conf_event = config.get("evenement", {})
|
|
|
|
smtp_host = conf_email.get("smtp_host", "")
|
|
smtp_port = conf_email.get("smtp_port", 587)
|
|
smtp_user = conf_email.get("smtp_user", "")
|
|
smtp_password = conf_email.get("smtp_password", "")
|
|
expediteur = conf_email.get("expediteur", smtp_user)
|
|
|
|
if not smtp_host or not smtp_user:
|
|
log.error("Configuration email incomplete")
|
|
return False
|
|
|
|
sujet = conf_email.get("sujet", "Votre photo - {evenement}").format(
|
|
evenement=conf_event.get("nom", "Photobooth")
|
|
)
|
|
message = conf_email.get("message", "Voici votre photo !").format(
|
|
evenement=conf_event.get("nom", "Photobooth")
|
|
)
|
|
|
|
msg = MIMEMultipart()
|
|
msg["From"] = expediteur
|
|
msg["To"] = destinataire
|
|
msg["Subject"] = sujet
|
|
msg.attach(MIMEText(message, "plain", "utf-8"))
|
|
|
|
# Piece jointe
|
|
if chemin_photo.exists():
|
|
with open(chemin_photo, "rb") as f:
|
|
piece = MIMEBase("image", "jpeg")
|
|
piece.set_payload(f.read())
|
|
encoders.encode_base64(piece)
|
|
piece.add_header(
|
|
"Content-Disposition",
|
|
f"attachment; filename={chemin_photo.name}"
|
|
)
|
|
msg.attach(piece)
|
|
|
|
try:
|
|
with smtplib.SMTP(smtp_host, smtp_port, timeout=10) as serveur:
|
|
serveur.starttls()
|
|
serveur.login(smtp_user, smtp_password)
|
|
serveur.send_message(msg)
|
|
log.info(f"Email envoye a {destinataire}")
|
|
_sauvegarder_email_historique(destinataire)
|
|
return True
|
|
except (smtplib.SMTPException, OSError) as e:
|
|
log.error(f"Erreur envoi email : {e}")
|
|
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())
|
|
|
|
|
|
EMAIL_RAPPORT = "contact@copydev.fr"
|
|
|
|
|
|
def traiter_spool() -> tuple[int, list[str]]:
|
|
"""Tente d'envoyer les emails en attente. Retourne (nb envoyes, liste destinataires)."""
|
|
with _spool_lock:
|
|
spool = _charger_spool()
|
|
if not spool:
|
|
return 0, []
|
|
|
|
envoyes = 0
|
|
destinataires = []
|
|
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
|
|
destinataires.append(entry["email"])
|
|
else:
|
|
restants.append(entry)
|
|
break
|
|
|
|
if envoyes > 0 or len(restants) < len(spool):
|
|
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, destinataires
|
|
|
|
|
|
def envoyer_rapport_spool(nb_envoyes: int, destinataires: list[str], nb_echoues: int):
|
|
"""Envoie un rapport a contact@copydev.fr apres vidage du spool."""
|
|
config = charger_config()
|
|
conf_email = config.get("email", {})
|
|
conf_event = config.get("evenement", {})
|
|
nom_event = conf_event.get("nom", "Photobooth")
|
|
|
|
smtp_host = conf_email.get("smtp_host", "")
|
|
smtp_port = conf_email.get("smtp_port", 587)
|
|
smtp_user = conf_email.get("smtp_user", "")
|
|
smtp_password = conf_email.get("smtp_password", "")
|
|
expediteur = conf_email.get("expediteur", smtp_user)
|
|
|
|
if not smtp_host or not smtp_user:
|
|
return
|
|
|
|
liste = "\n".join(f" - {d}" for d in destinataires)
|
|
corps = f"{nb_envoyes} photo(s) envoyée(s) par email.\n\n"
|
|
corps += f"Destinataires :\n{liste}\n"
|
|
if nb_echoues > 0:
|
|
corps += f"\n{nb_echoues} email(s) en échec (restent en file d'attente).\n"
|
|
|
|
msg = MIMEMultipart()
|
|
msg["From"] = expediteur
|
|
msg["To"] = EMAIL_RAPPORT
|
|
msg["Subject"] = f"[{nom_event}] {nb_envoyes} photo(s) envoyée(s)"
|
|
msg.attach(MIMEText(corps, "plain", "utf-8"))
|
|
|
|
try:
|
|
with smtplib.SMTP(smtp_host, smtp_port, timeout=10) as serveur:
|
|
serveur.starttls()
|
|
serveur.login(smtp_user, smtp_password)
|
|
serveur.send_message(msg)
|
|
log.info(f"Rapport spool envoye a {EMAIL_RAPPORT}")
|
|
except (smtplib.SMTPException, OSError) as e:
|
|
log.error(f"Erreur envoi rapport spool : {e}")
|
|
|
|
|
|
async def tache_spool_demarrage():
|
|
"""Tente de vider le spool une seule fois au demarrage du serveur."""
|
|
await asyncio.sleep(10)
|
|
total = taille_spool()
|
|
if total > 0:
|
|
log.info(f"Spool: tentative d'envoi au demarrage ({total} en attente)")
|
|
nb, dests = await asyncio.get_event_loop().run_in_executor(None, traiter_spool)
|
|
if nb > 0:
|
|
echoues = total - nb
|
|
await asyncio.get_event_loop().run_in_executor(
|
|
None, envoyer_rapport_spool, nb, dests, echoues
|
|
)
|
|
|
|
|
|
FICHIER_EMAILS = RACINE / "data" / "emails_history.json"
|
|
|
|
|
|
def _event_id_actif() -> str:
|
|
return charger_config().get("evenement", {}).get("event_id") or "_sans_evenement"
|
|
|
|
|
|
def _charger_toutes_historiques() -> dict:
|
|
if not FICHIER_EMAILS.exists():
|
|
return {}
|
|
try:
|
|
with open(FICHIER_EMAILS, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
# Compatibilite avec l'ancien format (liste globale non cloisonnee)
|
|
if isinstance(data, list):
|
|
return {"_sans_evenement": data}
|
|
return data
|
|
except (json.JSONDecodeError, OSError):
|
|
return {}
|
|
|
|
|
|
def _sauvegarder_email_historique(email: str):
|
|
toutes = _charger_toutes_historiques()
|
|
event_id = _event_id_actif()
|
|
historique = toutes.setdefault(event_id, [])
|
|
email_lower = email.lower().strip()
|
|
if email_lower not in historique:
|
|
historique.append(email_lower)
|
|
try:
|
|
with open(FICHIER_EMAILS, "w", encoding="utf-8") as f:
|
|
json.dump(toutes, f, ensure_ascii=False)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def charger_emails_historique() -> list:
|
|
return _charger_toutes_historiques().get(_event_id_actif(), [])
|
|
|
|
|
|
def effacer_emails_historique():
|
|
toutes = _charger_toutes_historiques()
|
|
toutes.pop(_event_id_actif(), None)
|
|
try:
|
|
with open(FICHIER_EMAILS, "w", encoding="utf-8") as f:
|
|
json.dump(toutes, f, ensure_ascii=False)
|
|
except OSError:
|
|
pass
|