From abfb1bec7f6b063775b99d705a68bf239ca07e09 Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Thu, 7 May 2026 22:37:22 +0200 Subject: [PATCH] feat(pct): bouton 'Telecharger .eml' pour New Outlook / Outlook Web Cas: poste sans Outlook desktop (New Outlook = WebView2, pas de COM ; SMTP filtre firewall corp). Solution: PatchCenter genere un fichier .eml MIME que l'user telecharge puis double-clique -> s'ouvre dans son client mail par defaut (New Outlook, Outlook Web, Thunderbird, etc.) pour relecture + envoi manuel. - Endpoint POST /patching/import/pct-prevenance/download-eml: construit un MIMEMultipart alternative avec From/To/Cc/Subject/Date/Message-ID + body HTML (le meme que pour SMTP) Retourne 'message/rfc822' avec Content-Disposition attachment - UI Modal Prevenance PCT: nouveau bouton violet 'Telecharger .eml' a cote de Annuler + 'Envoyer (SMTP)'. Fetch + Blob + URL.createObjectURL + a.download pour declencher le download cote navigateur --- app/routers/planning_import.py | 75 ++++++++++++++++++++++++++++++ app/templates/patching_import.html | 37 ++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/app/routers/planning_import.py b/app/routers/planning_import.py index 5418904..806ef85 100644 --- a/app/routers/planning_import.py +++ b/app/routers/planning_import.py @@ -1286,6 +1286,81 @@ async def pct_prevenance_preview(request: Request, db=Depends(get_db)): }) +@router.post("/patching/import/pct-prevenance/download-eml") +async def pct_prevenance_download_eml(request: Request, db=Depends(get_db)): + """Génère un fichier .eml (MIME) prêt à ouvrir dans le client mail par défaut. + Solution alternative quand SMTP / Outlook COM bloqués (firewall corp, pas d'Outlook desktop). + Body JSON : {row_ids: [int, ...]}""" + from fastapi.responses import Response + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + from email.utils import formataddr, formatdate, make_msgid + + user = get_current_user(request) + if not user: + return JSONResponse({"ok": False, "msg": "Non authentifié"}, status_code=401) + perms = get_user_perms(db, user) + if not (can_view(perms, "planning") or can_view(perms, "campaigns")): + return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403) + try: + body = await request.json() + except Exception: + return JSONResponse({"ok": False, "msg": "Body JSON invalide"}, status_code=400) + ids = [int(x) for x in (body.get("row_ids") or []) if str(x).isdigit()] + if not ids: + return JSONResponse({"ok": False, "msg": "Aucune ligne sélectionnée"}, status_code=400) + + placeholders = ",".join(str(i) for i in ids) + rows = db.execute(text(f""" + SELECT asset_name, application_name, environnement, domaine, + jour, heure_t, intervenant + FROM patch_planning_import_rows + WHERE id IN ({placeholders}) + ORDER BY jour NULLS LAST, heure_t NULLS LAST, asset_name + """)).fetchall() + if not rows: + return JSONResponse({"ok": False, "msg": "Lignes introuvables"}, status_code=404) + rows_dicts = [ + {"asset_name": r.asset_name, "application_name": r.application_name, + "environnement": r.environnement, "domaine": r.domaine, + "jour": r.jour, "heure_t": r.heure_t, "intervenant": r.intervenant} + for r in rows + ] + intervenant_name = (rows[0].intervenant or user.get("sub") or "SecOps") + subject, html = _build_pct_email(rows_dicts, intervenant_name=intervenant_name) + + from ..services.secrets_service import get_secret + pct_recipient = (get_secret(db, "smtp_pct_recipient") or "PCT.reims@sanef.com").strip() + cc_list = [c["email"] for c in _fetch_pct_cc_emails(db, ids)] + sender_email = (get_secret(db, "smtp_from") or "").strip() or f"{user.get('sub') or 'patchcenter'}@sanef.com" + + # Construit le MIME .eml + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = sender_email + msg["To"] = pct_recipient + if cc_list: + msg["Cc"] = ", ".join(cc_list) + msg["Date"] = formatdate(localtime=True) + msg["Message-ID"] = make_msgid(domain="patchcenter.sanef.local") + msg["X-Mailer"] = "PatchCenter (eml export)" + msg.attach(MIMEText(html, "html", "utf-8")) + + eml_bytes = msg.as_bytes() + # Nom de fichier : timestamp + nb serveurs + from datetime import datetime as _dt + ts = _dt.now().strftime("%Y%m%d_%H%M%S") + fname = f"prevenance_pct_{ts}_{len(rows)}srv.eml" + + return Response( + content=eml_bytes, + media_type="message/rfc822", + headers={ + "Content-Disposition": f'attachment; filename="{fname}"', + }, + ) + + @router.post("/patching/import/pct-prevenance/send") async def pct_prevenance_send(request: Request, db=Depends(get_db)): """Envoie réellement le mail PCT pour les rows sélectionnées. diff --git a/app/templates/patching_import.html b/app/templates/patching_import.html index 18d6340..b532a8f 100644 --- a/app/templates/patching_import.html +++ b/app/templates/patching_import.html @@ -543,6 +543,40 @@ }); document.getElementById('pct-btn-cancel')?.addEventListener('click', closePctModal); document.getElementById('pct-modal-bg')?.addEventListener('click', closePctModal); + + // Bouton "Télécharger .eml" : génère un fichier mail prêt à ouvrir dans Outlook web/New + document.getElementById('pct-btn-eml')?.addEventListener('click', async () => { + const checkedAny = Array.from(tbody.querySelectorAll('input.row-cb:checked')); + const ids = checkedAny.map(cb => cb.dataset.id); + if (!ids.length) { alert('Sélection vide'); return; } + try { + const r = await fetch('/patching/import/pct-prevenance/download-eml', { + method: 'POST', headers: {'Content-Type':'application/json'}, + credentials: 'same-origin', + body: JSON.stringify({row_ids: ids}), + }); + if (!r.ok) { + const j = await r.json().catch(()=>({})); + alert('Erreur génération .eml : ' + (j.msg || r.status)); + return; + } + // Récupère le nom de fichier depuis Content-Disposition + let filename = 'prevenance_pct.eml'; + const cd = r.headers.get('Content-Disposition') || ''; + const m = cd.match(/filename="?([^"]+)"?/i); + if (m) filename = m[1]; + const blob = await r.blob(); + // Force download + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = filename; + document.body.appendChild(a); a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } catch (e) { + alert('Erreur réseau : ' + e); + } + }); })(); {% endif %} @@ -602,7 +636,8 @@