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
This commit is contained in:
Pierre & Lumière 2026-05-07 22:37:22 +02:00
parent 97ac7681d2
commit abfb1bec7f
2 changed files with 111 additions and 1 deletions

View File

@ -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") @router.post("/patching/import/pct-prevenance/send")
async def pct_prevenance_send(request: Request, db=Depends(get_db)): async def pct_prevenance_send(request: Request, db=Depends(get_db)):
"""Envoie réellement le mail PCT pour les rows sélectionnées. """Envoie réellement le mail PCT pour les rows sélectionnées.

View File

@ -543,6 +543,40 @@
}); });
document.getElementById('pct-btn-cancel')?.addEventListener('click', closePctModal); document.getElementById('pct-btn-cancel')?.addEventListener('click', closePctModal);
document.getElementById('pct-modal-bg')?.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);
}
});
})(); })();
</script> </script>
{% endif %} {% endif %}
@ -602,7 +636,8 @@
</div> </div>
<div class="pct-footer"> <div class="pct-footer">
<button id="pct-btn-cancel" class="pct-btn pct-btn-cancel">Annuler</button> <button id="pct-btn-cancel" class="pct-btn pct-btn-cancel">Annuler</button>
<button id="pct-btn-send" class="pct-btn pct-btn-send">📤 Envoyer</button> <button id="pct-btn-eml" class="pct-btn" style="color:#a78bfa; border-color:#a78bfa; background:rgba(167,139,250,0.15);" title="Télécharge un .eml à ouvrir dans Outlook (Web/New) — relecture + envoi manuel">⬇ Télécharger .eml</button>
<button id="pct-btn-send" class="pct-btn pct-btn-send">📤 Envoyer (SMTP)</button>
</div> </div>
</div> </div>
</div> </div>