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:
parent
97ac7681d2
commit
abfb1bec7f
@ -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.
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user