Services: - EmailService: CSIRT notifications (24h/72h/30d), training alerts, welcome email - RateLimitService: File-based rate limiting for auth and AI endpoints - ReportService: Executive HTML report, CSV exports (risks/incidents/controls/assets) Integrations: - AuthController: Rate limiting on login (5/min, 20/h) and register (3/10min) - IncidentController: Email notifications on CSIRT milestones - AuditController: Executive report and CSV export endpoints - Router: 429 rate limit error handling, new audit export routes Database: - Migration 002: email_log table for notification tracking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
729 lines
38 KiB
PHP
729 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à</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;
|
|
|
|
$subject = "[URGENTE] Early Warning Incidente {$incident['incident_code'] ?? ''} - {$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à 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à</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à e il suo impatto, nonché 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;
|
|
|
|
$subject = "[NIS2] Notifica Incidente 72h {$incident['incident_code'] ?? ''} - {$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>È 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;
|
|
|
|
$subject = "[NIS2] Report Finale Incidente {$incident['incident_code'] ?? ''} - {$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 è stato assegnato un nuovo corso di formazione sulla sicurezza informatica nell'ambito del programma di conformità 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 è stata superata',
|
|
$daysLeft === 0 => 'La scadenza è 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 è 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à 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;">🔒</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> è stata completata con successo.</p>
|
|
|
|
<p>{$this->esc($this->appName)} è la piattaforma per la gestione della conformità alla Direttiva NIS2 (EU 2022/2555) e al D.Lgs. 138/2024. Con questa piattaforma potrà:</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à</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ò 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>È 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à 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ò 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à 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 è una notifica automatica inviata da <strong>{$this->esc($this->appName)}</strong>.<br>
|
|
Piattaforma di gestione della conformità 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à al Regolamento (UE) 2016/679 (GDPR).
|
|
</p>
|
|
<p style="margin: 12px 0 0; color: #94a3b8; font-size: 11px; text-align: center;">
|
|
© {$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',
|
|
'sent_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
} 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)),
|
|
};
|
|
}
|
|
}
|