diff --git a/admin.php b/admin.php
index f456b07..d96e99e 100644
--- a/admin.php
+++ b/admin.php
@@ -136,6 +136,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = "AI Co-Pilot instellingen zijn succesvol opgeslagen.";
$messageType = 'success';
}
+
+ // 5. Test e-mail versturen via SMTP (Release v1.6)
+ elseif ($action === 'send_test_email') {
+ require_once __DIR__ . '/includes/mailer.php';
+ $testEmail = trim($_POST['test_email'] ?? '');
+ if (empty($testEmail) || !filter_var($testEmail, FILTER_VALIDATE_EMAIL)) {
+ $message = "Vul een geldig e-mailadres in om een test e-mail te sturen.";
+ $messageType = 'error';
+ } else {
+ $mailResult = FocizMailer::sendTestEmail($testEmail, $currentUser['name']);
+ if ($mailResult['success']) {
+ $message = "Test e-mail succesvol verzonden via mail.fociz.nl naar " . htmlspecialchars($testEmail) . "!";
+ $messageType = 'success';
+ } else {
+ $message = "Fout bij verzenden test e-mail via mail.fociz.nl: " . htmlspecialchars($mailResult['error'] ?? 'Onbekende fout');
+ $messageType = 'error';
+ }
+ }
+ }
}
// Haal actuele configuratie op
@@ -646,6 +665,73 @@ require_once __DIR__ . '/includes/header.php';
+
+
+
-
Wachtwoord
+
diff --git a/auth/reset-password.php b/auth/reset-password.php
new file mode 100644
index 0000000..89fa560
--- /dev/null
+++ b/auth/reset-password.php
@@ -0,0 +1,280 @@
+prepare("
+ SELECT pr.*, u.name, u.email as user_email
+ FROM password_resets pr
+ JOIN users u ON pr.email = u.email
+ WHERE pr.token = ? AND pr.used_at IS NULL AND pr.expires_at > NOW()
+ ");
+ $stmt->execute([$token]);
+ $resetRecord = $stmt->fetch();
+}
+
+$error = null;
+$success = false;
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && $resetRecord) {
+ $password = $_POST['password'] ?? '';
+ $confirmPassword = $_POST['confirm_password'] ?? '';
+
+ if (empty($password)) {
+ $error = "Vul een nieuw wachtwoord in.";
+ } elseif (strlen($password) < 8) {
+ $error = "Het wachtwoord moet minimaal 8 tekens lang zijn.";
+ } elseif ($password !== $confirmPassword) {
+ $error = "De wachtwoorden komen niet overeen.";
+ } else {
+ try {
+ $db->beginTransaction();
+
+ $newHash = password_hash($password, PASSWORD_DEFAULT);
+
+ // 1. Update wachtwoord van de gebruiker en reset remember tokens
+ $stmtUser = $db->prepare("
+ UPDATE users
+ SET password_hash = ?, remember_token = NULL, remember_token_expires_at = NULL
+ WHERE email = ?
+ ");
+ $stmtUser->execute([$newHash, $resetRecord['user_email']]);
+
+ // 2. Markeer reset token als verbruikt
+ $stmtToken = $db->prepare("UPDATE password_resets SET used_at = NOW() WHERE token = ?");
+ $stmtToken->execute([$token]);
+
+ $db->commit();
+ $success = true;
+
+ } catch (Throwable $e) {
+ if ($db->inTransaction()) $db->rollBack();
+ error_log("Fout bij resetten wachtwoord: " . $e->getMessage());
+ $error = "Er is een fout opgetreden bij het opslaan van uw nieuwe wachtwoord.";
+ }
+ }
+}
+
+$pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
+?>
+
+
+
+
+
+
= htmlspecialchars($pageTitle) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Herstellink Ongeldig
+
+ Deze wachtwoordherstellink is ongeldig, verlopen (langer dan 60 minuten geleden aangevraagd) of al eerder gebruikt.
+
+
+
+
+
+
+
+
+
+
+
Wachtwoord Gewijzigd!
+
+ Uw nieuwe wachtwoord is succesvol opgeslagen. U kunt nu direct inloggen in Fociz.
+
+
+ Inloggen met Nieuw Wachtwoord
+
+
+
+
+
+
+
+
+
+
+
Nieuw Wachtwoord
+
+ Kies een nieuw veilig wachtwoord voor = htmlspecialchars($resetRecord['user_email']) ?>
+
+
+
+
+
+
+ = htmlspecialchars($error) ?>
+
+
+
+
+
+
+
+
+
+ Beveiligd met end-to-end authenticatie • Fociz 2026
+
+
+
+
+
+
+
diff --git a/config/.env.example b/config/.env.example
index e87ef4a..b930d6c 100644
--- a/config/.env.example
+++ b/config/.env.example
@@ -25,4 +25,14 @@ JIRA_DOMAIN=""
JIRA_EMAIL=""
JIRA_API_TOKEN=""
+# SMTP Mail Server
+SMTP_HOST="mail.fociz.nl"
+SMTP_PORT="587"
+SMTP_USER="info@fociz.nl"
+SMTP_PASS=""
+SMTP_SECURE="tls"
+SMTP_FROM_EMAIL="no-reply@fociz.nl"
+SMTP_FROM_NAME="Fociz"
+SMTP_REPLY_TO="info@fociz.nl"
+
diff --git a/config/config.php b/config/config.php
index b3ca0c8..127691e 100644
--- a/config/config.php
+++ b/config/config.php
@@ -128,6 +128,18 @@ define('APP_URL', fociz_detect_app_url());
// Tijdzone
date_default_timezone_set('Europe/Amsterdam');
+// =========================================================================
+// 3b. SMTP MAIL SERVER CONFIGURATIE
+// =========================================================================
+define('SMTP_HOST', fociz_env('SMTP_HOST', 'mail.fociz.nl'));
+define('SMTP_PORT', (int)fociz_env('SMTP_PORT', 587));
+define('SMTP_USER', fociz_env('SMTP_USER', 'info@fociz.nl'));
+define('SMTP_PASS', fociz_env('SMTP_PASS', '6swpQSBg5uWFmjar7F6a'));
+define('SMTP_SECURE', fociz_env('SMTP_SECURE', 'tls')); // 'tls' (STARTTLS port 587) of 'ssl' (port 465)
+define('SMTP_FROM_EMAIL', fociz_env('SMTP_FROM_EMAIL', 'no-reply@fociz.nl'));
+define('SMTP_FROM_NAME', fociz_env('SMTP_FROM_NAME', 'Fociz'));
+define('SMTP_REPLY_TO', fociz_env('SMTP_REPLY_TO', 'info@fociz.nl'));
+
// =========================================================================
// 4. VEILIGE FOUTAFHANDELING & PEN TEST BESCHERMING
// =========================================================================
diff --git a/config/database.php b/config/database.php
index 74462a2..77d4cd4 100644
--- a/config/database.php
+++ b/config/database.php
@@ -148,6 +148,7 @@ function ensure_tables_exist(PDO $pdo): void {
"ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme",
"ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled",
"ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)",
+ "CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
];
foreach ($colUpgrades as $sql) {
try {
diff --git a/config/schema.sql b/config/schema.sql
index 992abbe..d49f3c2 100644
--- a/config/schema.sql
+++ b/config/schema.sql
@@ -31,6 +31,17 @@ CREATE TABLE IF NOT EXISTS `invites` (
CONSTRAINT `fk_invites_user` FOREIGN KEY (`created_by_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+CREATE TABLE IF NOT EXISTS `password_resets` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `email` VARCHAR(191) NOT NULL,
+ `token` VARCHAR(64) NOT NULL UNIQUE,
+ `expires_at` DATETIME NOT NULL,
+ `used_at` DATETIME NULL,
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ INDEX (`email`),
+ INDEX (`token`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
CREATE TABLE IF NOT EXISTS `daily_plans` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
diff --git a/includes/mailer.php b/includes/mailer.php
new file mode 100644
index 0000000..b5fd43c
--- /dev/null
+++ b/includes/mailer.php
@@ -0,0 +1,597 @@
+ 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 = "
+
+
+ " . ($ctaSubtext ? "
{$ctaSubtext}
" : "") . "
+
+ Werkt de knop niet? Kopieer en plak dan deze link in uw browser:
+ {$ctaUrl}
+
+ ";
+ }
+
+ return "
+
+
+
+
+
{$title}
+
+
+
+
+
+
+ {$preheader}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {$title}
+
+
+
+ {$contentHtml}
+
+
+ {$ctaBlock}
+
+
+
+
+
+
+
+
+ 🔒
+
+
+ {$securityNotice}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © {$currentYear} Fociz • Veilige Project- & Dagplanning
+
+
+ Dit is een geautomatiseerd systeembericht verzonden naar u door Fociz.
+
+
+
+
+
+
+
+
+
+";
+ }
+
+ /**
+ * 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 = "
+
Beste {$cleanName} ,
+
+ Er is zojuist een verzoek gedaan om het wachtwoord van uw Fociz-account opnieuw in te stellen voor het e-mailadres {$email} .
+
+
+ Klik op onderstaande knop om direct een nieuw wachtwoord te kiezen. Deze link is uit veiligheidsoverwegingen 60 minuten geldig .
+
+ ";
+
+ $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 = "
+
Hallo,
+
+ {$cleanInviter} heeft u uitgenodigd om toegang te krijgen tot de project- en dagplanner van Fociz .
+
+
+
+
+ E-mailadres:
+ {$email}
+
+
+ Toegewezen rol:
+ {$roleLabel}
+
+
+
+
+ Klik op onderstaande knop om uw account te activeren, uw wachtwoord in te stellen en de verplichte tweestapsverificatie (2FA) te koppelen.
+
+ ";
+
+ $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 = "
+
Beste {$cleanName} ,
+
+ Gefeliciteerd! Als u dit bericht leest, is de verbinding met de Fociz SMTP mailserver volledig geslaagd en operationeel.
+
+
+
+
+ SMTP Server:
+ {$smtpHost}:{$smtpPort}
+
+
+ Verzendadres:
+ {$fromEmail}
+
+
+ Tijdstip:
+ {$currentTime}
+
+
+
+
+ Wachtwoord-vergeten e-mails, teamuitnodigingen en notificaties kunnen nu veilig en betrouwbaar worden verzonden.
+
+ ";
+
+ $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('/