[FIX] EmailService: invio via relay AgileHub (X-Internal-Key) invece di mail()
Fase 0 modulo questionari fornitori + fix bug produzione. mail() built-in e' VIETATA dallo standard email-relay v1.0 e non recapitava nel container. EmailService::send() ora instrada tutte le email via POST /api/emails/send-raw del relay centralizzato email-automation-ms, header X-Internal-Key, env multi-source (workaround clear_env PHP-FPM Alpine, pattern SsoHelper::postInternal). Email mascherate nei log (GDPR, maskEmail()). Beneficiano tutti i 6 caller esistenti senza modifiche: sendQuestionnaire (supply-chain), forgotPassword (auth), notifiche incidenti, formazione, feedback, contact. Smoke test E2E produzione: send() => TRUE, email_log status=SENT (product=nis2). Hot-reload USR2 su nis2-app. version.json -> 1.8.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
aa2db4c6c2
commit
54576119f3
@ -3,7 +3,8 @@
|
||||
* NIS2 Agile - Email Service
|
||||
*
|
||||
* Gestione notifiche email per compliance NIS2.
|
||||
* Utilizza mail() nativa con Postfix/sendmail su server Hetzner.
|
||||
* Invio via relay centralizzato AgileHub (email-automation-ms) con X-Internal-Key.
|
||||
* Standard: docs/STANDARD_EMAIL_RELAY.md (mail() built-in VIETATA).
|
||||
*
|
||||
* Notifiche supportate:
|
||||
* - Early warning incidenti (24h, Art. 23 NIS2)
|
||||
@ -39,29 +40,105 @@ class EmailService
|
||||
* @param string $subject Oggetto email
|
||||
* @param string $htmlBody Contenuto HTML (inserito nel template)
|
||||
* @param string|null $from Indirizzo mittente (opzionale)
|
||||
* @return bool true se mail() ha accettato il messaggio
|
||||
* @return bool true se il relay ha accettato il messaggio
|
||||
*/
|
||||
public function send(string $to, string $subject, string $htmlBody, ?string $from = null): bool
|
||||
{
|
||||
$from = $from ?? $this->defaultFrom;
|
||||
|
||||
$headers = implode("\r\n", [
|
||||
'From: ' . $this->appName . ' <' . $from . '>',
|
||||
'Reply-To: ' . $from,
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'X-Mailer: NIS2 Agile',
|
||||
]);
|
||||
|
||||
$fullHtml = $this->wrapInTemplate($subject, $htmlBody);
|
||||
|
||||
$result = @mail($to, '=?UTF-8?B?' . base64_encode($subject) . '?=', $fullHtml, $headers);
|
||||
// Standard email-relay v1.0 (AgileHub): mail() built-in e VIETATO e non
|
||||
// recapita nel container. L'unico canale valido e il relay centralizzato
|
||||
// (email-automation-ms) via X-Internal-Key. Vedi docs/STANDARD_EMAIL_RELAY.md.
|
||||
$result = $this->sendViaRelay($to, $subject, $fullHtml, $from);
|
||||
|
||||
$this->logEmail($to, $subject, $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invia via relay centralizzato AgileHub (POST /api/emails/send-raw).
|
||||
* cURL nativo, header X-Internal-Key. Pattern adattato da SsoHelper::postInternal.
|
||||
*
|
||||
* @return bool true se il relay ha accettato il messaggio (HTTP 2xx + success)
|
||||
*/
|
||||
private function sendViaRelay(string $to, string $subject, string $fullHtml, string $from): bool
|
||||
{
|
||||
$base = rtrim(self::env('EMAIL_MS_URL', 'https://agilehub.agile.software/api/emails'), '/');
|
||||
$key = self::env('INTERNAL_EMAIL_KEY', '');
|
||||
|
||||
if ($key === '') {
|
||||
error_log('[EmailService] INTERNAL_EMAIL_KEY non configurata: invio email saltato per ' . self::maskEmail($to));
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = json_encode([
|
||||
'to' => $to,
|
||||
'subject' => $subject,
|
||||
'html' => $fullHtml,
|
||||
'product' => 'nis2',
|
||||
'reply_to' => $from,
|
||||
'priority' => 'transactional',
|
||||
]);
|
||||
|
||||
$ch = curl_init($base . '/send-raw');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'X-Internal-Key: ' . $key,
|
||||
],
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errno !== 0) {
|
||||
error_log('[EmailService] relay unreachable (' . curl_strerror($errno) . ') per ' . self::maskEmail($to));
|
||||
return false;
|
||||
}
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
error_log('[EmailService] relay HTTP ' . $httpCode . ' per ' . self::maskEmail($to) . ': ' . substr((string) $body, 0, 200));
|
||||
return false;
|
||||
}
|
||||
|
||||
$decoded = json_decode((string) $body, true);
|
||||
if (is_array($decoded) && array_key_exists('success', $decoded) && !$decoded['success']) {
|
||||
error_log('[EmailService] relay success=false per ' . self::maskEmail($to) . ': ' . substr((string) $body, 0, 200));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Maschera l'email per i log (GDPR): m***@dominio.it. */
|
||||
private static function maskEmail(string $email): string
|
||||
{
|
||||
$at = strpos($email, '@');
|
||||
if ($at === false || $at === 0) return '***';
|
||||
return $email[0] . '***' . substr($email, $at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-source env lookup (workaround PHP-FPM Alpine: getenv() puo' essere
|
||||
* false nei worker FPM con clear_env=yes). Stesso pattern di SsoHelper/VectorService.
|
||||
*/
|
||||
private static function env(string $key, string $default): string
|
||||
{
|
||||
$v = getenv($key);
|
||||
if ($v !== false && $v !== '') return $v;
|
||||
if (!empty($_SERVER[$key])) return (string) $_SERVER[$key];
|
||||
if (!empty($_ENV[$key])) return (string) $_ENV[$key];
|
||||
return $default;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// NOTIFICHE INCIDENTI (Art. 23 NIS2)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@ -1 +1 @@
|
||||
{"version":"1.7.0","build":"20260529h","date":"2026-05-29T16:30:00+02:00","changelog":"FEAT integrazione analisi docs/nis2: (1) Asset Relevance Scoring NIS2 0-100 a 6 criteri (GV.OC-04) + registro formale stampabile; (2) Tassonomia incidenti Determina ACN 164179/2025 (IS-1..4, regime essenziale/importante Allegati 3-4); (3) Post-Incident Review strutturato 5-Whys + metriche TTD/TTC/TTR; (4) Layer mapping NIST CSF 2.0 (43 controlli); (5) Fonti normative certe: registry citabile + grounding AI + citazioni help + ingest PDF normativi nella KB RAG."}
|
||||
{"version":"1.8.0","build":"2026-05-31-v1.8.0","date":"2026-05-31","changelog":"Fase 0 modulo questionari fornitori: EmailService instrada via relay centralizzato AgileHub (X-Internal-Key) al posto di mail() (vietata/non recapitava). Fix invio email per sendQuestionnaire, forgotPassword, notifiche incidenti, formazione, feedback, contact."}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user