nis2-agile/application/services/EmailService.php
DevEnv nis2-agile 13df162ec4 [FIX] SIM-06 + EmailService + WebhookService + supplier assessment
- ServicesController::provision(): created_by usa userId (INT) non string
- EmailService::logEmail(): rimosso sent_at (colonna non esiste in email_log)
- WebhookService::incidentPayload(): status ?? 'detected' (null-safe)
- simulate-nis2.php: supplier assessment usa formato assessment_responses
  corretto [{question, weight, value: yes|partial|no}]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 09:56:39 +01:00

731 lines
38 KiB
PHP

<?php
/**
* NIS2 Agile - Email Service
*
* Gestione notifiche email per compliance NIS2.
* Utilizza mail() nativa con Postfix/sendmail su server Hetzner.
*
* Notifiche supportate:
* - Early warning incidenti (24h, Art. 23 NIS2)
* - Notifica incidenti (72h)
* - Report finale incidenti (30 giorni)
* - Assegnazione e promemoria formazione
* - Avvisi scadenze generiche
* - Benvenuto registrazione
* - Invito organizzazione
*/
class EmailService
{
private string $defaultFrom;
private string $appName;
private string $appUrl;
public function __construct()
{
$this->appName = APP_NAME;
$this->appUrl = APP_URL;
$this->defaultFrom = 'noreply@' . parse_url($this->appUrl, PHP_URL_HOST);
}
// ═══════════════════════════════════════════════════════════════════════════
// INVIO EMAIL
// ═══════════════════════════════════════════════════════════════════════════
/**
* Invia una email HTML con template professionale
*
* @param string $to Indirizzo destinatario
* @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
*/
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);
$this->logEmail($to, $subject, $result);
return $result;
}
// ═══════════════════════════════════════════════════════════════════════════
// NOTIFICHE INCIDENTI (Art. 23 NIS2)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Notifica early warning 24h (Art. 23 par. 4 lett. a)
*
* @param array $incident Dati incidente (title, incident_code, severity, detected_at, classification)
* @param array $organization Dati organizzazione (name, sector)
* @param array $recipients Lista di indirizzi email destinatari
*/
public function sendIncidentEarlyWarning(array $incident, array $organization, array $recipients): void
{
$severityLabel = $this->translateSeverity($incident['severity'] ?? 'medium');
$detectedAt = $this->formatDateTime($incident['detected_at'] ?? '');
$deadline = $this->formatDateTime($incident['early_warning_due'] ?? '');
$html = <<<HTML
<div style="background-color: #fef2f2; border-left: 4px solid #dc2626; padding: 16px; margin-bottom: 24px; border-radius: 4px;">
<strong style="color: #dc2626; font-size: 16px;">EARLY WARNING - Preallarme 24 ore</strong><br>
<span style="color: #991b1b;">Ai sensi dell'Art. 23, par. 4, lett. a) della Direttiva NIS2</span>
</div>
<p>Si comunica che l'organizzazione <strong>{$this->esc($organization['name'])}</strong> ha rilevato un incidente significativo che richiede la notifica obbligatoria al CSIRT nazionale (ACN).</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600; width: 200px;">Codice Incidente</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['incident_code'] ?? '-')}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Titolo</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['title'] ?? '-')}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Classificazione</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['classification'] ?? '-')}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Gravit&agrave;</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($severityLabel)}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Rilevato il</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($detectedAt)}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Scadenza Early Warning</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; color: #dc2626; font-weight: 600;">{$this->esc($deadline)}</td>
</tr>
</table>
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 14px; margin: 20px 0; border-radius: 4px;">
<strong>Azione richiesta:</strong> Inviare il preallarme al CSIRT nazionale (ACN) entro 24 ore dalla rilevazione dell'incidente, indicando se l'incidente sia sospettato di essere causato da atti illegittimi o malevoli e se possa avere un impatto transfrontaliero.
</div>
<p style="margin-top: 20px;">
<a href="{$this->esc($this->appUrl)}/dashboard/incidents" style="display: inline-block; background-color: #1e40af; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Gestisci Incidente</a>
</p>
HTML;
$incidentCode = $incident['incident_code'] ?? '';
$subject = "[URGENTE] Early Warning Incidente {$incidentCode} - {$organization['name']}";
foreach ($recipients as $email) {
$this->send($email, $subject, $html);
}
}
/**
* Notifica incidente 72h (Art. 23 par. 4 lett. b)
*
* @param array $incident Dati incidente
* @param array $organization Dati organizzazione
* @param array $recipients Lista indirizzi email
*/
public function sendIncidentNotification(array $incident, array $organization, array $recipients): void
{
$severityLabel = $this->translateSeverity($incident['severity'] ?? 'medium');
$detectedAt = $this->formatDateTime($incident['detected_at'] ?? '');
$deadline = $this->formatDateTime($incident['notification_due'] ?? '');
$html = <<<HTML
<div style="background-color: #fff7ed; border-left: 4px solid #ea580c; padding: 16px; margin-bottom: 24px; border-radius: 4px;">
<strong style="color: #ea580c; font-size: 16px;">NOTIFICA INCIDENTE - Termine 72 ore</strong><br>
<span style="color: #9a3412;">Ai sensi dell'Art. 23, par. 4, lett. b) della Direttiva NIS2</span>
</div>
<p>Si comunica la necessit&agrave; di inviare la notifica formale dell'incidente significativo al CSIRT nazionale (ACN) per l'organizzazione <strong>{$this->esc($organization['name'])}</strong>.</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600; width: 200px;">Codice Incidente</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['incident_code'] ?? '-')}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Titolo</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['title'] ?? '-')}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Gravit&agrave;</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($severityLabel)}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Rilevato il</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($detectedAt)}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Servizi Interessati</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['affected_services'] ?? 'Non specificati')}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Scadenza Notifica</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; color: #ea580c; font-weight: 600;">{$this->esc($deadline)}</td>
</tr>
</table>
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 14px; margin: 20px 0; border-radius: 4px;">
<strong>Azione richiesta:</strong> Aggiornare la notifica al CSIRT nazionale (ACN) entro 72 ore dalla rilevazione, fornendo una valutazione iniziale dell'incidente, compresa la sua gravit&agrave; e il suo impatto, nonch&eacute; gli indicatori di compromissione, ove disponibili.
</div>
<p style="margin-top: 20px;">
<a href="{$this->esc($this->appUrl)}/dashboard/incidents" style="display: inline-block; background-color: #1e40af; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Gestisci Incidente</a>
</p>
HTML;
$incidentCode = $incident['incident_code'] ?? '';
$subject = "[NIS2] Notifica Incidente 72h {$incidentCode} - {$organization['name']}";
foreach ($recipients as $email) {
$this->send($email, $subject, $html);
}
}
/**
* Notifica report finale 30 giorni (Art. 23 par. 4 lett. d)
*
* @param array $incident Dati incidente
* @param array $organization Dati organizzazione
* @param array $recipients Lista indirizzi email
*/
public function sendIncidentFinalReport(array $incident, array $organization, array $recipients): void
{
$detectedAt = $this->formatDateTime($incident['detected_at'] ?? '');
$deadline = $this->formatDateTime($incident['final_report_due'] ?? '');
$html = <<<HTML
<div style="background-color: #eff6ff; border-left: 4px solid #1e40af; padding: 16px; margin-bottom: 24px; border-radius: 4px;">
<strong style="color: #1e40af; font-size: 16px;">REPORT FINALE - Termine 30 giorni</strong><br>
<span style="color: #1e3a8a;">Ai sensi dell'Art. 23, par. 4, lett. d) della Direttiva NIS2</span>
</div>
<p>&Egrave; necessario predisporre e trasmettere il report finale dell'incidente al CSIRT nazionale (ACN) per l'organizzazione <strong>{$this->esc($organization['name'])}</strong>.</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600; width: 200px;">Codice Incidente</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['incident_code'] ?? '-')}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Titolo</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['title'] ?? '-')}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Rilevato il</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($detectedAt)}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Causa Radice</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['root_cause'] ?? 'Da determinare')}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Azioni di Rimedio</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($incident['remediation_actions'] ?? 'Da documentare')}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Scadenza Report Finale</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; color: #1e40af; font-weight: 600;">{$this->esc($deadline)}</td>
</tr>
</table>
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 14px; margin: 20px 0; border-radius: 4px;">
<strong>Azione richiesta:</strong> Predisporre il report finale entro 30 giorni dalla notifica dell'incidente, contenente: descrizione dettagliata dell'incidente, tipo di minaccia o causa radice, misure di attenuazione applicate e in corso, eventuale impatto transfrontaliero.
</div>
<p style="margin-top: 20px;">
<a href="{$this->esc($this->appUrl)}/dashboard/incidents" style="display: inline-block; background-color: #1e40af; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Prepara Report Finale</a>
</p>
HTML;
$incidentCode = $incident['incident_code'] ?? '';
$subject = "[NIS2] Report Finale Incidente {$incidentCode} - {$organization['name']}";
foreach ($recipients as $email) {
$this->send($email, $subject, $html);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// NOTIFICHE FORMAZIONE (Art. 20 NIS2)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Notifica assegnazione corso di formazione
*
* @param array $assignment Dati assegnazione (due_date)
* @param array $user Dati utente (full_name, email)
* @param array $course Dati corso (title, description, duration_minutes, is_mandatory)
*/
public function sendTrainingAssignment(array $assignment, array $user, array $course): void
{
$dueDate = $this->formatDate($assignment['due_date'] ?? '');
$duration = (int) ($course['duration_minutes'] ?? 0);
$mandatoryText = !empty($course['is_mandatory']) ? 'Obbligatorio' : 'Facoltativo';
$html = <<<HTML
<div style="background-color: #f0fdf4; border-left: 4px solid #16a34a; padding: 16px; margin-bottom: 24px; border-radius: 4px;">
<strong style="color: #16a34a; font-size: 16px;">Nuovo corso di formazione assegnato</strong><br>
<span style="color: #166534;">Formazione cybersecurity ai sensi dell'Art. 20 della Direttiva NIS2</span>
</div>
<p>Gentile <strong>{$this->esc($user['full_name'])}</strong>,</p>
<p>Le &egrave; stato assegnato un nuovo corso di formazione sulla sicurezza informatica nell'ambito del programma di conformit&agrave; NIS2 della Sua organizzazione.</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600; width: 200px;">Corso</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($course['title'] ?? '-')}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Descrizione</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($course['description'] ?? '-')}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Durata Stimata</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$duration} minuti</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Tipo</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($mandatoryText)}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Scadenza</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">{$this->esc($dueDate)}</td>
</tr>
</table>
<p style="margin-top: 20px;">
<a href="{$this->esc($this->appUrl)}/dashboard/training" style="display: inline-block; background-color: #16a34a; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Inizia il Corso</a>
</p>
HTML;
$subject = "Formazione assegnata: {$course['title']}";
$this->send($user['email'], $subject, $html);
}
/**
* Promemoria scadenza formazione
*
* @param array $assignment Dati assegnazione (due_date)
* @param array $user Dati utente (full_name, email)
* @param array $course Dati corso (title, is_mandatory)
*/
public function sendTrainingReminder(array $assignment, array $user, array $course): void
{
$dueDate = $this->formatDate($assignment['due_date'] ?? '');
$daysLeft = $this->daysUntil($assignment['due_date'] ?? '');
$urgencyColor = match (true) {
$daysLeft <= 1 => '#dc2626',
$daysLeft <= 3 => '#ea580c',
$daysLeft <= 7 => '#f59e0b',
default => '#1e40af',
};
$daysText = match (true) {
$daysLeft < 0 => 'La scadenza &egrave; stata superata',
$daysLeft === 0 => 'La scadenza &egrave; oggi',
$daysLeft === 1 => 'Manca 1 giorno alla scadenza',
default => "Mancano {$daysLeft} giorni alla scadenza",
};
$html = <<<HTML
<div style="background-color: #fffbeb; border-left: 4px solid {$urgencyColor}; padding: 16px; margin-bottom: 24px; border-radius: 4px;">
<strong style="color: {$urgencyColor}; font-size: 16px;">Promemoria Formazione</strong><br>
<span style="color: #92400e;">{$daysText}</span>
</div>
<p>Gentile <strong>{$this->esc($user['full_name'])}</strong>,</p>
<p>Le ricordiamo che il corso di formazione <strong>{$this->esc($course['title'])}</strong> deve essere completato entro il <strong>{$this->esc($dueDate)}</strong>.</p>
<p>Il completamento della formazione sulla sicurezza informatica &egrave; un requisito previsto dall'Art. 20 della Direttiva NIS2 e dal programma di compliance della Sua organizzazione.</p>
<p style="margin-top: 20px;">
<a href="{$this->esc($this->appUrl)}/dashboard/training" style="display: inline-block; background-color: {$urgencyColor}; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Completa il Corso</a>
</p>
HTML;
$subject = "Promemoria: {$course['title']} - scadenza {$dueDate}";
$this->send($user['email'], $subject, $html);
}
// ═══════════════════════════════════════════════════════════════════════════
// AVVISI SCADENZE
// ═══════════════════════════════════════════════════════════════════════════
/**
* Avviso generico di scadenza
*
* @param string $type Tipo scadenza (assessment, policy_review, training, incident_report, audit, ecc.)
* @param string $title Titolo descrittivo della scadenza
* @param string $dueDate Data di scadenza (Y-m-d o Y-m-d H:i:s)
* @param array $recipients Lista indirizzi email
*/
public function sendDeadlineAlert(string $type, string $title, string $dueDate, array $recipients): void
{
$formattedDate = $this->formatDate($dueDate);
$daysLeft = $this->daysUntil($dueDate);
$typeLabel = $this->translateDeadlineType($type);
$urgencyColor = match (true) {
$daysLeft <= 1 => '#dc2626',
$daysLeft <= 3 => '#ea580c',
$daysLeft <= 7 => '#f59e0b',
default => '#1e40af',
};
$daysText = match (true) {
$daysLeft < 0 => 'Scadenza superata',
$daysLeft === 0 => 'Scade oggi',
$daysLeft === 1 => 'Scade domani',
default => "Scade tra {$daysLeft} giorni",
};
$html = <<<HTML
<div style="background-color: #f8fafc; border-left: 4px solid {$urgencyColor}; padding: 16px; margin-bottom: 24px; border-radius: 4px;">
<strong style="color: {$urgencyColor}; font-size: 16px;">Avviso Scadenza - {$this->esc($typeLabel)}</strong><br>
<span style="color: #64748b;">{$daysText}</span>
</div>
<p>Si segnala la seguente scadenza relativa al programma di conformit&agrave; NIS2:</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600; width: 200px;">Tipo</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($typeLabel)}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Descrizione</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($title)}</td>
</tr>
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Scadenza</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; color: {$urgencyColor}; font-weight: 600;">{$this->esc($formattedDate)}</td>
</tr>
</table>
<p style="margin-top: 20px;">
<a href="{$this->esc($this->appUrl)}/dashboard" style="display: inline-block; background-color: #1e40af; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Vai alla Dashboard</a>
</p>
HTML;
$subject = "[Scadenza] {$typeLabel}: {$title} - {$formattedDate}";
foreach ($recipients as $email) {
$this->send($email, $subject, $html);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// BENVENUTO E INVITI
// ═══════════════════════════════════════════════════════════════════════════
/**
* Email di benvenuto dopo la registrazione
*
* @param array $user Dati utente (full_name, email)
*/
public function sendWelcome(array $user): void
{
$html = <<<HTML
<div style="text-align: center; margin-bottom: 30px;">
<div style="font-size: 48px; margin-bottom: 10px;">&#128274;</div>
<h2 style="color: #1e40af; margin: 0;">Benvenuto in {$this->esc($this->appName)}!</h2>
</div>
<p>Gentile <strong>{$this->esc($user['full_name'])}</strong>,</p>
<p>La Sua registrazione su <strong>{$this->esc($this->appName)}</strong> &egrave; stata completata con successo.</p>
<p>{$this->esc($this->appName)} &egrave; la piattaforma per la gestione della conformit&agrave; alla Direttiva NIS2 (EU 2022/2555) e al D.Lgs. 138/2024. Con questa piattaforma potr&agrave;:</p>
<ul style="line-height: 1.8; color: #374151;">
<li>Effettuare la <strong>Gap Analysis</strong> rispetto ai requisiti NIS2</li>
<li>Gestire il <strong>Risk Assessment</strong> con matrice di rischio</li>
<li>Gestire gli <strong>incidenti di sicurezza</strong> con i flussi di notifica Art. 23</li>
<li>Generare <strong>policy di sicurezza</strong> personalizzate</li>
<li>Monitorare la <strong>supply chain</strong> e i fornitori critici</li>
<li>Gestire la <strong>formazione</strong> del personale (Art. 20)</li>
<li>Preparare <strong>audit e documentazione</strong> per le autorit&agrave;</li>
</ul>
<p style="margin-top: 24px;">
<a href="{$this->esc($this->appUrl)}/dashboard" style="display: inline-block; background-color: #1e40af; color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 16px;">Accedi alla Piattaforma</a>
</p>
<p style="margin-top: 24px; color: #6b7280; font-size: 14px;">Se non ha effettuato questa registrazione, pu&ograve; ignorare questa email.</p>
HTML;
$subject = "Benvenuto in {$this->appName}";
$this->send($user['email'], $subject, $html);
}
/**
* Invito a unirsi a un'organizzazione
*
* @param array $user Dati utente invitato (full_name, email)
* @param array $organization Dati organizzazione (name)
* @param string $role Ruolo assegnato
*/
public function sendMemberInvite(array $user, array $organization, string $role): void
{
$roleLabel = $this->translateRole($role);
$html = <<<HTML
<div style="background-color: #eff6ff; border-left: 4px solid #1e40af; padding: 16px; margin-bottom: 24px; border-radius: 4px;">
<strong style="color: #1e40af; font-size: 16px;">Invito a collaborare</strong><br>
<span style="color: #1e3a8a;">{$this->esc($organization['name'])}</span>
</div>
<p>Gentile <strong>{$this->esc($user['full_name'])}</strong>,</p>
<p>&Egrave; stata invitata/o a unirsi all'organizzazione <strong>{$this->esc($organization['name'])}</strong> sulla piattaforma <strong>{$this->esc($this->appName)}</strong> per la gestione della conformit&agrave; NIS2.</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background-color: #f8fafc;">
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600; width: 200px;">Organizzazione</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($organization['name'])}</td>
</tr>
<tr>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0; font-weight: 600;">Ruolo Assegnato</td>
<td style="padding: 10px 14px; border: 1px solid #e2e8f0;">{$this->esc($roleLabel)}</td>
</tr>
</table>
<p style="margin-top: 20px;">
<a href="{$this->esc($this->appUrl)}/dashboard" style="display: inline-block; background-color: #1e40af; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Accetta Invito</a>
</p>
<p style="margin-top: 20px; color: #6b7280; font-size: 14px;">Se non riconosce questa richiesta, pu&ograve; ignorare questa email.</p>
HTML;
$subject = "Invito: unisciti a {$organization['name']} su {$this->appName}";
$this->send($user['email'], $subject, $html);
}
// ═══════════════════════════════════════════════════════════════════════════
// TEMPLATE HTML
// ═══════════════════════════════════════════════════════════════════════════
/**
* Avvolge il contenuto HTML nel template email professionale
*/
private function wrapInTemplate(string $subject, string $bodyHtml): string
{
$year = date('Y');
return <<<HTML
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{$this->esc($subject)}</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f1f5f9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -webkit-font-smoothing: antialiased;">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f1f5f9;">
<tr>
<td align="center" style="padding: 30px 15px;">
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="max-width: 600px; width: 100%;">
<!-- Header -->
<tr>
<td style="background-color: #1e40af; padding: 28px 32px; border-radius: 8px 8px 0 0; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 22px; font-weight: 700; letter-spacing: 0.5px;">{$this->esc($this->appName)}</h1>
<p style="margin: 6px 0 0; color: #bfdbfe; font-size: 13px; font-weight: 400;">Piattaforma di Conformit&agrave; NIS2</p>
</td>
</tr>
<!-- Content -->
<tr>
<td style="background-color: #ffffff; padding: 32px; border-left: 1px solid #e2e8f0; border-right: 1px solid #e2e8f0; color: #1e293b; font-size: 15px; line-height: 1.6;">
{$bodyHtml}
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8fafc; padding: 24px 32px; border-radius: 0 0 8px 8px; border: 1px solid #e2e8f0; border-top: none;">
<p style="margin: 0 0 12px; color: #64748b; font-size: 13px; line-height: 1.5; text-align: center;">
Questa &egrave; una notifica automatica inviata da <strong>{$this->esc($this->appName)}</strong>.<br>
Piattaforma di gestione della conformit&agrave; alla Direttiva NIS2 (EU 2022/2555).
</p>
<p style="margin: 0 0 12px; text-align: center;">
<a href="{$this->esc($this->appUrl)}" style="color: #1e40af; text-decoration: none; font-size: 13px;">{$this->esc($this->appUrl)}</a>
</p>
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 16px 0;">
<p style="margin: 0; color: #94a3b8; font-size: 11px; line-height: 1.5; text-align: center;">
Questa email e i suoi allegati sono riservati e destinati esclusivamente ai destinatari indicati.
Se ha ricevuto questa comunicazione per errore, La preghiamo di contattare il mittente e cancellare il messaggio.
La informiamo che il trattamento dei dati personali avviene in conformit&agrave; al Regolamento (UE) 2016/679 (GDPR).
</p>
<p style="margin: 12px 0 0; color: #94a3b8; font-size: 11px; text-align: center;">
&copy; {$year} {$this->esc($this->appName)} - Tutti i diritti riservati
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
HTML;
}
// ═══════════════════════════════════════════════════════════════════════════
// LOGGING
// ═══════════════════════════════════════════════════════════════════════════
/**
* Registra l'invio email nel database per audit
*/
private function logEmail(string $to, string $subject, bool $success): void
{
try {
Database::insert('email_log', [
'recipient' => $to,
'subject' => mb_substr($subject, 0, 255),
'status' => $success ? 'sent' : 'failed',
]);
} catch (\Throwable $e) {
error_log('[EmailService] Errore log email: ' . $e->getMessage());
}
}
// ═══════════════════════════════════════════════════════════════════════════
// UTILITA'
// ═══════════════════════════════════════════════════════════════════════════
/**
* Escape HTML per prevenire XSS nei contenuti dinamici
*/
private function esc(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* Formatta data e ora in formato italiano
*/
private function formatDateTime(string $datetime): string
{
if (empty($datetime)) {
return '-';
}
$ts = strtotime($datetime);
if ($ts === false) {
return $datetime;
}
return date('d/m/Y H:i', $ts);
}
/**
* Formatta solo la data in formato italiano
*/
private function formatDate(string $date): string
{
if (empty($date)) {
return '-';
}
$ts = strtotime($date);
if ($ts === false) {
return $date;
}
return date('d/m/Y', $ts);
}
/**
* Calcola i giorni rimanenti fino a una data
*/
private function daysUntil(string $date): int
{
if (empty($date)) {
return 0;
}
$target = strtotime(date('Y-m-d', strtotime($date)));
$today = strtotime(date('Y-m-d'));
if ($target === false) {
return 0;
}
return (int) (($target - $today) / 86400);
}
/**
* Traduce il livello di gravita' in italiano
*/
private function translateSeverity(string $severity): string
{
return match ($severity) {
'low' => 'Bassa',
'medium' => 'Media',
'high' => 'Alta',
'critical' => 'Critica',
default => ucfirst($severity),
};
}
/**
* Traduce il tipo di scadenza in italiano
*/
private function translateDeadlineType(string $type): string
{
return match ($type) {
'assessment' => 'Gap Analysis',
'policy_review' => 'Revisione Policy',
'training' => 'Formazione',
'incident_report' => 'Report Incidente',
'audit' => 'Audit',
'risk_review' => 'Revisione Rischi',
'supply_chain' => 'Valutazione Supply Chain',
default => ucfirst(str_replace('_', ' ', $type)),
};
}
/**
* Traduce il ruolo organizzativo in italiano
*/
private function translateRole(string $role): string
{
return match ($role) {
'org_admin' => 'Amministratore Organizzazione',
'compliance_manager' => 'Compliance Manager',
'risk_manager' => 'Risk Manager',
'it_manager' => 'IT Manager',
'employee' => 'Collaboratore',
'auditor' => 'Auditor',
'viewer' => 'Osservatore',
default => ucfirst(str_replace('_', ' ', $role)),
};
}
}