Add password reset functionality and SMTP mailer class

- Implemented reset-password.php for users to reset their passwords securely.
- Added validation for reset tokens and password strength checks.
- Created mailer.php to handle sending emails via SMTP, including password reset and invitation emails.
- Integrated responsive HTML email templates for better user experience.
This commit is contained in:
2026-09-10 09:57:53 +02:00
parent d4fd706eca
commit a7aad57e0b
10 changed files with 1248 additions and 5 deletions
+597
View File
@@ -0,0 +1,597 @@
<?php
/**
* Fociz Pure-PHP SMTP Mailer & Responsive HTML E-mail Engine
*
* Volledig zelfstandige, veilige SMTP-client zonder externe afhankelijkheden.
* Ondersteunt STARTTLS (poort 587), SSL (poort 465), AUTH LOGIN/PLAIN en
* levert responsive, hoogwaardig gestylede HTML e-mails in de Fociz-huisstijl.
*/
require_once __DIR__ . '/../config/config.php';
class FocizMailer {
/**
* Verstuurt een e-mail via native PHP stream sockets over SMTP.
*
* @param string $toEmail Ontvanger e-mailadres
* @param string $toName Ontvanger weergavenaam
* @param string $subject Onderwerp van het bericht
* @param string $htmlBody Opgemaakte HTML inhoud
* @param string|null $plainTextBody Optionele platte tekst (wordt anders afgeleid uit HTML)
* @param array $options Extra opties (from_email, from_name, reply_to, headers)
* @return array ['success' => bool, 'error' => ?string, 'logs' => array]
*/
public static function send(
string $toEmail,
string $toName,
string $subject,
string $htmlBody,
?string $plainTextBody = null,
array $options = []
): array {
$logs = [];
$log = function(string $msg) use (&$logs) {
$logs[] = date('H:i:s') . ' ' . $msg;
};
// 1. Configuratieparameters ophalen
$host = $options['host'] ?? (defined('SMTP_HOST') ? SMTP_HOST : 'mail.fociz.nl');
$port = (int)($options['port'] ?? (defined('SMTP_PORT') ? SMTP_PORT : 587));
$username = $options['username'] ?? (defined('SMTP_USER') ? SMTP_USER : 'info@fociz.nl');
$password = $options['password'] ?? (defined('SMTP_PASS') ? SMTP_PASS : '');
$secure = strtolower($options['secure'] ?? (defined('SMTP_SECURE') ? SMTP_SECURE : 'tls'));
$fromEmail = $options['from_email'] ?? (defined('SMTP_FROM_EMAIL') ? SMTP_FROM_EMAIL : 'no-reply@fociz.nl');
$fromName = $options['from_name'] ?? (defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Fociz');
$replyTo = $options['reply_to'] ?? (defined('SMTP_REPLY_TO') ? SMTP_REPLY_TO : 'info@fociz.nl');
$timeout = (int)($options['timeout'] ?? 15);
// Technische afzender voor de SMTP envelope (moet doorgaans overeenkomen met het geauthenticeerde account)
$envelopeFrom = !empty($username) && filter_var($username, FILTER_VALIDATE_EMAIL) ? $username : $fromEmail;
if (empty($plainTextBody)) {
$plainTextBody = self::htmlToPlainText($htmlBody);
}
$log("Verbinding initialiseren naar {$host}:{$port} (beveiliging: {$secure})...");
// 2. Socket verbinden
$socketContext = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
]
]);
$remoteTarget = ($secure === 'ssl' || $port === 465) ? "ssl://{$host}:{$port}" : "tcp://{$host}:{$port}";
$errno = 0;
$errstr = '';
$socket = @stream_socket_client(
$remoteTarget,
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT,
$socketContext
);
if (!$socket) {
$err = "Kan geen socketverbinding openen naar {$host}:{$port} [{$errno}]: {$errstr}";
$log("FOUT: {$err}");
error_log("FocizMailer: " . $err);
return ['success' => false, 'error' => $err, 'logs' => $logs];
}
stream_set_timeout($socket, $timeout);
// Helperfuncties voor SMTP commando's
$readResponse = function() use ($socket, $log): array {
$data = '';
$code = 0;
while (!feof($socket)) {
$line = fgets($socket, 1024);
if ($line === false) break;
$data .= $line;
if (preg_match('/^([0-9]{3})([ -])(.*)$/', $line, $matches)) {
$code = (int)$matches[1];
if ($matches[2] === ' ') {
// Laatste regel van het antwoord
break;
}
}
}
$log("<< " . trim(preg_replace('/\r?\n/', ' | ', $data)));
return ['code' => $code, 'text' => $data];
};
$sendCommand = function(string $cmd, string $maskLog = '') use ($socket, $log, $readResponse): array {
$logMsg = !empty($maskLog) ? $maskLog : $cmd;
$log(">> " . trim($logMsg));
fwrite($socket, $cmd . "\r\n");
return $readResponse();
};
try {
// 3. Welkomstbanner lezen (220)
$resp = $readResponse();
if ($resp['code'] !== 220) {
throw new Exception("Ongeldige server banner (verwacht 220, kreeg {$resp['code']}): {$resp['text']}");
}
// 4. EHLO Handshake
$clientHost = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'fociz.nl';
$resp = $sendCommand("EHLO {$clientHost}");
if ($resp['code'] !== 250) {
$resp = $sendCommand("HELO {$clientHost}");
if ($resp['code'] !== 250) {
throw new Exception("EHLO/HELO geweigerd door server: {$resp['text']}");
}
}
// 5. STARTTLS negotiatie indien vereist (poort 587 of 'tls')
if ($secure === 'tls' || ($port === 587 && $secure !== 'none')) {
$log("STARTTLS handshake starten...");
$resp = $sendCommand("STARTTLS");
if ($resp['code'] !== 220) {
throw new Exception("STARTTLS geweigerd door server (code {$resp['code']}): {$resp['text']}");
}
// Crypto activeren op de actieve stream
$cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
$cryptoMethod |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
}
if (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT')) {
$cryptoMethod |= STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
}
$cryptoSuccess = @stream_socket_enable_crypto($socket, true, $cryptoMethod);
if (!$cryptoSuccess) {
throw new Exception("TLS encryptiehandshake kon niet tot stand worden gebracht.");
}
$log("TLS encryptie succesvol geactiveerd.");
// Na STARTTLS moet EHLO opnieuw worden gestuurd (RFC 3207)
$resp = $sendCommand("EHLO {$clientHost}");
if ($resp['code'] !== 250) {
throw new Exception("EHLO na TLS handshake geweigerd: {$resp['text']}");
}
}
// 6. SMTP Authenticatie
if (!empty($username) && !empty($password)) {
$log("Authenticatie uitvoeren als '{$username}'...");
$resp = $sendCommand("AUTH LOGIN");
if ($resp['code'] === 334) {
// Stuur gebruikersnaam
$resp = $sendCommand(base64_encode($username), "AUTH USER [base64]");
if ($resp['code'] !== 334) {
throw new Exception("Gebruikersnaam geweigerd voor authenticatie: {$resp['text']}");
}
// Stuur wachtwoord
$resp = $sendCommand(base64_encode($password), "AUTH PASS [base64 masked]");
if ($resp['code'] !== 235) {
throw new Exception("Wachtwoord onjuist of authenticatie mislukt (code {$resp['code']}): {$resp['text']}");
}
} else {
// Fallback naar AUTH PLAIN
$plainToken = base64_encode("\0" . $username . "\0" . $password);
$resp = $sendCommand("AUTH PLAIN {$plainToken}", "AUTH PLAIN [token masked]");
if ($resp['code'] !== 235) {
throw new Exception("SMTP authenticatie geweigerd (code {$resp['code']}): {$resp['text']}");
}
}
$log("SMTP Authenticatie geslaagd.");
}
// 7. Afzender en Ontvanger instellen
$resp = $sendCommand("MAIL FROM:<{$envelopeFrom}>");
if ($resp['code'] !== 250) {
// Probeer met fromEmail indien envelopeFrom faalt
if ($envelopeFrom !== $fromEmail) {
$resp = $sendCommand("MAIL FROM:<{$fromEmail}>");
}
if ($resp['code'] !== 250) {
throw new Exception("MAIL FROM geweigerd (code {$resp['code']}): {$resp['text']}");
}
}
$resp = $sendCommand("RCPT TO:<{$toEmail}>");
if ($resp['code'] !== 250 && $resp['code'] !== 251) {
throw new Exception("RCPT TO geweigerd voor {$toEmail} (code {$resp['code']}): {$resp['text']}");
}
// 8. DATA Commando
$resp = $sendCommand("DATA");
if ($resp['code'] !== 354) {
throw new Exception("Server niet gereed voor e-maildata (code {$resp['code']}): {$resp['text']}");
}
// 9. MIME Multipart/Alternative Samenstellen
$boundary = "----=_Part_Fociz_" . md5(uniqid((string)time(), true));
$encodedSubject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
$encodedFromName = "=?UTF-8?B?" . base64_encode($fromName) . "?=";
$encodedToName = !empty($toName) ? "=?UTF-8?B?" . base64_encode($toName) . "?= " : "";
$headers = [];
$headers[] = "Date: " . date(DATE_RFC2822);
$headers[] = "From: {$encodedFromName} <{$fromEmail}>";
$headers[] = "To: {$encodedToName}<{$toEmail}>";
if (!empty($replyTo)) {
$headers[] = "Reply-To: <{$replyTo}>";
}
$headers[] = "Subject: {$encodedSubject}";
$headers[] = "Message-ID: <" . time() . "." . bin2hex(random_bytes(8)) . "@" . ($clientHost ?: 'fociz.nl') . ">";
$headers[] = "X-Mailer: Fociz Mail Engine 2026";
$headers[] = "MIME-Version: 1.0";
$headers[] = "Content-Type: multipart/alternative; boundary=\"{$boundary}\"";
$rawEmail = implode("\r\n", $headers) . "\r\n\r\n";
// Deel 1: Platte tekst fallback
$rawEmail .= "--{$boundary}\r\n";
$rawEmail .= "Content-Type: text/plain; charset=UTF-8; format=flowed\r\n";
$rawEmail .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$rawEmail .= $plainTextBody . "\r\n\r\n";
// Deel 2: Luxe HTML variant
$rawEmail .= "--{$boundary}\r\n";
$rawEmail .= "Content-Type: text/html; charset=UTF-8\r\n";
$rawEmail .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$rawEmail .= $htmlBody . "\r\n\r\n";
// Sluiting boundary
$rawEmail .= "--{$boundary}--\r\n";
// Zorg voor veilige dot-stuffing (regels die beginnen met een punt krijgen een extra punt)
$safeLines = [];
foreach (explode("\n", str_replace("\r\n", "\n", $rawEmail)) as $line) {
if (isset($line[0]) && $line[0] === '.') {
$line = '.' . $line;
}
$safeLines[] = $line;
}
$finalPayload = implode("\r\n", $safeLines) . "\r\n.\r\n";
// Schrijf e-mail inhoud
$log("E-maildata overdragen (" . strlen($finalPayload) . " bytes)...");
fwrite($socket, $finalPayload);
$resp = $readResponse();
if ($resp['code'] !== 250) {
throw new Exception("Fout bij accepteren van e-maildata door de server (code {$resp['code']}): {$resp['text']}");
}
$log("E-mail succesvol geaccepteerd voor aflevering door {$host}.");
// 10. QUIT Commando
$sendCommand("QUIT");
@fclose($socket);
return [
'success' => true,
'error' => null,
'logs' => $logs
];
} catch (Throwable $e) {
$log("FOUT: " . $e->getMessage());
error_log("FocizMailer Fout: " . $e->getMessage());
@fwrite($socket, "QUIT\r\n");
@fclose($socket);
return [
'success' => false,
'error' => $e->getMessage(),
'logs' => $logs
];
}
}
/**
* Genereert een hoogwaardig gestylede, responsive HTML e-mail template
* in de kenmerkende moderne Fociz donker/licht huisstijl.
*/
public static function renderEmailTemplate(array $params): string {
$preheader = htmlspecialchars($params['preheader'] ?? 'Bericht vanuit Fociz');
$badge = htmlspecialchars($params['badge'] ?? 'FOCIZ SYSTEEM');
$title = htmlspecialchars($params['title'] ?? 'Belangrijk bericht van Fociz');
$contentHtml = $params['content_html'] ?? '';
$ctaText = !empty($params['cta_text']) ? htmlspecialchars($params['cta_text']) : null;
$ctaUrl = !empty($params['cta_url']) ? htmlspecialchars($params['cta_url']) : null;
$ctaSubtext = !empty($params['cta_subtext']) ? htmlspecialchars($params['cta_subtext']) : null;
$securityNotice = $params['security_notice'] ?? 'Heeft u dit verzoek niet zelf ingediend? Dan kunt u deze e-mail veilig negeren. Uw gegevens blijven optimaal beschermd.';
$currentYear = date('Y');
$appUrl = defined('APP_URL') ? APP_URL : 'https://www.fociz.nl';
$ctaBlock = '';
if ($ctaText && $ctaUrl) {
$ctaBlock = "
<!-- Call To Action Knop -->
<table role='presentation' border='0' cellpadding='0' cellspacing='0' style='margin: 32px 0 24px 0;'>
<tr>
<td align='center' style='border-radius: 12px; background: #4f46e5;'>
<a href='{$ctaUrl}' target='_blank' style='display: inline-block; padding: 14px 32px; font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif; font-size: 15px; font-weight: 700; color: #ffffff; text-decoration: none; border-radius: 12px; background: #4f46e5; letter-spacing: 0.2px;'>
{$ctaText} &rarr;
</a>
</td>
</tr>
</table>
" . ($ctaSubtext ? "<p style='margin: 0 0 20px 0; font-size: 12px; color: #64748b; line-height: 1.5;'>{$ctaSubtext}</p>" : "") . "
<p style='margin: 16px 0 0 0; font-size: 11px; color: #94a3b8; word-break: break-all; line-height: 1.5;'>
Werkt de knop niet? Kopieer en plak dan deze link in uw browser:<br>
<a href='{$ctaUrl}' style='color: #6366f1; text-decoration: underline;'>{$ctaUrl}</a>
</p>
";
}
return "<!DOCTYPE html>
<html lang='nl'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>{$title}</title>
<!--[if mso]>
<style type='text/css'>
body, table, td, a { font-family: Arial, sans-serif !important; }
</style>
<![endif]-->
<style>
body { margin: 0; padding: 0; min-width: 100%; background-color: #0c0f17; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }
img { border: 0; outline: none; text-decoration: none; }
table { border-collapse: collapse; }
td { border-collapse: collapse; }
@media only screen and (max-width: 600px) {
.email-container { width: 100% !important; padding: 12px !important; }
.card-content { padding: 24px 20px !important; }
.header-banner { padding: 28px 20px !important; }
}
</style>
</head>
<body style='margin: 0; padding: 0; background-color: #0c0f17; -webkit-font-smoothing: antialiased;'>
<!-- Verborgen Pre-header tekst voor inbox preview -->
<div style='display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: #0c0f17; opacity: 0;'>
{$preheader} &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;
</div>
<table role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #0c0f17;'>
<tr>
<td align='center' style='padding: 40px 16px 50px 16px;'>
<!-- Hoofdcontainer -->
<table role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='max-width: 580px;' class='email-container'>
<!-- Luxe Bovenkaart -->
<tr>
<td style='background: #111622; border-top-left-radius: 20px; border-top-right-radius: 20px; border: 1px solid #1e293b; border-bottom: none; padding: 36px 36px 32px 36px;' class='header-banner'>
<table role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%'>
<tr>
<td>
<!-- Logo & Titel -->
<table role='presentation' border='0' cellpadding='0' cellspacing='0'>
<tr>
<td style='width: 44px; height: 44px; background: linear-gradient(135deg, #4f46e5 0%, #312e81 100%); border-radius: 12px; text-align: center; vertical-align: middle; border: 1px solid #6366f1; box-shadow: 0 4px 12px rgba(79, 70, 229, 0.35);'>
<span style='font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; font-size: 22px; font-weight: 900; color: #ffffff; line-height: 44px; display: inline-block;'>F</span>
</td>
<td style='padding-left: 14px;'>
<span style='font-size: 22px; font-weight: 900; color: #ffffff; letter-spacing: -0.5px; display: block;'>Fociz</span>
<span style='font-size: 11px; font-weight: 500; color: #94a3b8; letter-spacing: 0.5px; text-transform: uppercase;'>Focus op wat telt</span>
</td>
</tr>
</table>
</td>
<td align='right' style='vertical-align: top;'>
<!-- Badge -->
<span style='display: inline-block; padding: 5px 12px; font-size: 10px; font-weight: 700; color: #818cf8; background: rgba(99, 102, 241, 0.12); border: 1px solid rgba(99, 102, 241, 0.28); border-radius: 20px; letter-spacing: 0.6px; text-transform: uppercase;'>
{$badge}
</span>
</td>
</tr>
</table>
</td>
</tr>
<!-- Hoofdkaart met Inhoud -->
<tr>
<td style='background: #151d2f; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; border: 1px solid #1e293b; border-top: 1px solid #243048; padding: 36px;' class='card-content'>
<h1 style='margin: 0 0 16px 0; font-size: 22px; font-weight: 800; color: #ffffff; letter-spacing: -0.4px; line-height: 1.3;'>
{$title}
</h1>
<div style='font-size: 14px; line-height: 1.7; color: #cbd5e1;'>
{$contentHtml}
</div>
{$ctaBlock}
<!-- Beveiligings- & Waarschuwingsbox -->
<table role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='margin-top: 32px; border-top: 1px solid #1e293b; padding-top: 20px;'>
<tr>
<td style='background: rgba(15, 23, 42, 0.6); border: 1px solid #243048; border-radius: 12px; padding: 14px 16px;'>
<table role='presentation' border='0' cellpadding='0' cellspacing='0'>
<tr>
<td style='vertical-align: top; padding-right: 10px; font-size: 16px;'>
🔒
</td>
<td style='font-size: 12px; line-height: 1.5; color: #94a3b8;'>
{$securityNotice}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td align='center' style='padding: 28px 20px 0 20px;'>
<p style='margin: 0 0 6px 0; font-size: 12px; color: #64748b; font-weight: 500;'>
&copy; {$currentYear} Fociz &bull; Veilige Project- &amp; Dagplanning
</p>
<p style='margin: 0; font-size: 11px; color: #475569;'>
Dit is een geautomatiseerd systeembericht verzonden naar u door Fociz.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>";
}
/**
* Verstuurt een 'Wachtwoord Vergeten' herstelmail
*/
public static function sendPasswordResetEmail(string $email, string $name, string $resetUrl): array {
$cleanName = !empty($name) ? htmlspecialchars($name) : 'Collega';
$title = "Herstel uw Fociz wachtwoord";
$preheader = "U heeft een verzoek ingediend om uw Fociz wachtwoord opnieuw in te stellen.";
$contentHtml = "
<p style='margin: 0 0 14px 0;'>Beste <strong>{$cleanName}</strong>,</p>
<p style='margin: 0 0 14px 0;'>
Er is zojuist een verzoek gedaan om het wachtwoord van uw Fociz-account opnieuw in te stellen voor het e-mailadres <strong style='color: #e2e8f0;'>{$email}</strong>.
</p>
<p style='margin: 0 0 14px 0;'>
Klik op onderstaande knop om direct een nieuw wachtwoord te kiezen. Deze link is uit veiligheidsoverwegingen <strong>60 minuten geldig</strong>.
</p>
";
$html = self::renderEmailTemplate([
'title' => $title,
'preheader' => $preheader,
'badge' => 'VEILIGHEID',
'content_html' => $contentHtml,
'cta_text' => 'Wachtwoord Opnieuw Instellen',
'cta_url' => $resetUrl,
'cta_subtext' => '⏱️ Deze herstellink verloopt over 60 minuten en kan slechts éénmaal gebruikt worden.',
'security_notice' => 'Heeft u dit verzoek niet zelf gedaan? Geen zorgen, uw wachtwoord blijft ongewijzigd en uw account is veilig. U kunt deze e-mail gerust negeren.'
]);
return self::send($email, $name, "Herstel uw Fociz wachtwoord", $html);
}
/**
* Verstuurt een team-uitnodiging naar een nieuwe collega
*/
public static function sendInvitationEmail(
string $email,
string $inviterName,
string $role,
string $inviteUrl
): array {
$cleanInviter = !empty($inviterName) ? htmlspecialchars($inviterName) : 'Een beheerder';
$roleLabel = ($role === 'ADMIN') ? 'Beheerder (Admin)' : 'Teamlid (Member)';
$title = "U bent uitgenodigd voor Fociz";
$preheader = "{$cleanInviter} heeft u uitgenodigd om samen te werken in Fociz.";
$contentHtml = "
<p style='margin: 0 0 14px 0;'>Hallo,</p>
<p style='margin: 0 0 14px 0;'>
<strong>{$cleanInviter}</strong> heeft u uitgenodigd om toegang te krijgen tot de project- en dagplanner van <strong>Fociz</strong>.
</p>
<div style='background: rgba(99, 102, 241, 0.08); border: 1px solid rgba(99, 102, 241, 0.25); border-radius: 12px; padding: 14px 18px; margin: 18px 0;'>
<table role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%'>
<tr>
<td style='font-size: 13px; color: #94a3b8;'>E-mailadres:</td>
<td align='right' style='font-size: 13px; font-weight: 700; color: #e2e8f0;'>{$email}</td>
</tr>
<tr>
<td style='font-size: 13px; color: #94a3b8; padding-top: 6px;'>Toegewezen rol:</td>
<td align='right' style='font-size: 13px; font-weight: 700; color: #818cf8; padding-top: 6px;'>{$roleLabel}</td>
</tr>
</table>
</div>
<p style='margin: 0 0 14px 0;'>
Klik op onderstaande knop om uw account te activeren, uw wachtwoord in te stellen en de verplichte tweestapsverificatie (2FA) te koppelen.
</p>
";
$html = self::renderEmailTemplate([
'title' => $title,
'preheader' => $preheader,
'badge' => 'TEAM UITNODIGING',
'content_html' => $contentHtml,
'cta_text' => 'Account Activeren & Inloggen',
'cta_url' => $inviteUrl,
'cta_subtext' => '⏱️ Deze uitnodigingslink is 7 dagen geldig.',
'security_notice' => 'Fociz maakt gebruik van tweestapsverificatie (2FA). Houd een authenticator app zoals Google Authenticator of Microsoft Authenticator bij de hand.'
]);
return self::send($email, '', "Uitnodiging voor Fociz door {$cleanInviter}", $html);
}
/**
* Verstuurt een test e-mail ter verificatie van de SMTP verbinding
*/
public static function sendTestEmail(string $email, ?string $testerName = null): array {
$cleanName = !empty($testerName) ? htmlspecialchars($testerName) : 'Beheerder';
$title = "Fociz Mail Server Test Succesvol!";
$preheader = "De SMTP mailserver koppeling met mail.fociz.nl functioneert naar behoren.";
$smtpHost = defined('SMTP_HOST') ? SMTP_HOST : 'mail.fociz.nl';
$smtpPort = defined('SMTP_PORT') ? SMTP_PORT : 587;
$fromEmail = defined('SMTP_FROM_EMAIL') ? SMTP_FROM_EMAIL : 'no-reply@fociz.nl';
$currentTime = date('d-m-Y H:i:s');
$contentHtml = "
<p style='margin: 0 0 14px 0;'>Beste <strong>{$cleanName}</strong>,</p>
<p style='margin: 0 0 14px 0;'>
Gefeliciteerd! Als u dit bericht leest, is de verbinding met de Fociz SMTP mailserver volledig geslaagd en operationeel.
</p>
<div style='background: rgba(16, 185, 129, 0.08); border: 1px solid rgba(16, 185, 129, 0.25); border-radius: 12px; padding: 14px 18px; margin: 18px 0;'>
<table role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%'>
<tr>
<td style='font-size: 13px; color: #94a3b8;'>SMTP Server:</td>
<td align='right' style='font-size: 13px; font-weight: 700; color: #e2e8f0;'>{$smtpHost}:{$smtpPort}</td>
</tr>
<tr>
<td style='font-size: 13px; color: #94a3b8; padding-top: 6px;'>Verzendadres:</td>
<td align='right' style='font-size: 13px; font-weight: 700; color: #10b981; padding-top: 6px;'>{$fromEmail}</td>
</tr>
<tr>
<td style='font-size: 13px; color: #94a3b8; padding-top: 6px;'>Tijdstip:</td>
<td align='right' style='font-size: 13px; font-weight: 700; color: #e2e8f0; padding-top: 6px;'>{$currentTime}</td>
</tr>
</table>
</div>
<p style='margin: 0;'>
Wachtwoord-vergeten e-mails, teamuitnodigingen en notificaties kunnen nu veilig en betrouwbaar worden verzonden.
</p>
";
$html = self::renderEmailTemplate([
'title' => $title,
'preheader' => $preheader,
'badge' => 'VERIFICATIE TEST',
'content_html' => $contentHtml,
'cta_text' => 'Naar Fociz Dashboard',
'cta_url' => (defined('APP_URL') ? APP_URL : 'https://www.fociz.nl') . '/admin.php',
'security_notice' => 'Dit is een diagnostische testmail verzonden vanuit het Fociz Beheerpaneel.'
]);
return self::send($email, $testerName ?? '', "Fociz Mail Server Test Succesvol ({$currentTime})", $html);
}
/**
* Eenvoudige converter van HTML naar schone platte tekst voor e-mail fallbacks
*/
private static function htmlToPlainText(string $html): string {
$text = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $html);
$text = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $text);
$text = strip_tags($text, '<a><p><br><h1><h2><h3><li>');
$text = preg_replace('/<br\s*\/?>/i', "\n", $text);
$text = preg_replace('/<\/(p|h1|h2|h3|li)>/i', "\n\n", $text);
$text = strip_tags($text);
$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
return trim(preg_replace("/[\r\n]{3,}/", "\n\n", $text));
}
}