From 8f49bb17cd4500f37e08e228d6be24a4cf0f269d Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 22 Jan 2026 20:54:28 +0100 Subject: [PATCH 1/2] Add email sending logging functionality Add petitionId parameter to email functions Update email sending calls to include petitionId Add error message parameter to internal email functions Improve error handling in email sending functions Update thank you email to use signature email from CSV Add SMTP connection error logging to error message Add SMTP config not found error logging Add SMTP disabled error logging Add signature not found error logging --- custom/plugins/page/petition-form.php | 86 +++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/custom/plugins/page/petition-form.php b/custom/plugins/page/petition-form.php index b54f91b..6ece4dd 100644 --- a/custom/plugins/page/petition-form.php +++ b/custom/plugins/page/petition-form.php @@ -584,6 +584,45 @@ function petitionFormatDate(int $timestamp, Context $ctx): string { return "{$day}. {$month} {$year}, {$time}"; } +/** + * Log email send attempt to SMTP log file + * Used for tracking failed sends and enabling retry via CLI + */ +function petitionLogEmail(string $type, string $petitionId, string $email, bool $success, string $errorMessage = ''): void { + $logPath = dirname(__DIR__, 2) . '/data/smtp-log.csv'; + $dir = dirname($logPath); + + if (!is_dir($dir)) { + mkdir($dir, 0750, true); + } + + $isNewFile = !file_exists($logPath); + + $fp = fopen($logPath, 'a'); + if (!$fp) { + return; + } + + if (flock($fp, LOCK_EX)) { + if ($isNewFile) { + fputcsv($fp, ['timestamp', 'type', 'petition_id', 'email', 'status', 'error_message'], ',', '"', ''); + } + + fputcsv($fp, [ + time(), + $type, + $petitionId, + $email, + $success ? 'success' : 'failed', + $errorMessage + ], ',', '"', ''); + + flock($fp, LOCK_UN); + } + + fclose($fp); +} + /** * Retry wrapper for email sending with exponential backoff * Retries up to maxRetries times with increasing delays @@ -617,26 +656,33 @@ function petitionSendWithRetry(callable $sendFunction, int $maxRetries = 3): boo /** * Send confirmation email (with retry wrapper) */ -function petitionSendConfirmationEmail(array $data, string $confirmUrl, string $petitionTitle, Context $ctx): bool { - return petitionSendWithRetry(function() use ($data, $confirmUrl, $petitionTitle, $ctx) { - return petitionSendConfirmationEmailInternal($data, $confirmUrl, $petitionTitle, $ctx); +function petitionSendConfirmationEmail(array $data, string $confirmUrl, string $petitionTitle, string $petitionId, Context $ctx): bool { + $result = petitionSendWithRetry(function() use ($data, $confirmUrl, $petitionTitle, $ctx, &$errorMessage) { + return petitionSendConfirmationEmailInternal($data, $confirmUrl, $petitionTitle, $ctx, $errorMessage); }); + + // Log the final result + petitionLogEmail('confirmation', $petitionId, $data['email'], $result, $result ? '' : ($errorMessage ?? 'Unknown error')); + + return $result; } /** * Internal: Send confirmation email (single attempt) */ -function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl, string $petitionTitle, Context $ctx): bool { +function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl, string $petitionTitle, Context $ctx, ?string &$errorMessage = null): bool { // Decode HTML entities for plain-text email (e.g., – → –) $petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php'; if (!file_exists($smtpConfigPath)) { + $errorMessage = 'SMTP config not found'; return false; } $config = require $smtpConfigPath; if (!$config['enabled']) { + $errorMessage = 'SMTP disabled'; return false; } @@ -670,7 +716,7 @@ function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl, // Pre-flight check $fp = @fsockopen($config['host'], $config['port'], $errno, $errstr, 10); if (!$fp) { - // Note: Logging SMTP config only, no user data + $errorMessage = "Connection failed: {$errno} - {$errstr}"; error_log("Petition SMTP pre-flight failed: {$config['host']}:{$config['port']} - {$errno} - {$errstr}"); return false; } @@ -697,14 +743,14 @@ function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl, $output = ob_get_clean(); if (!$sent || stripos($output, 'error') !== false || stripos($output, '✗') !== false) { - // Note: Output is already sanitized with strip_tags, no user data exposed + $errorMessage = strip_tags($output) ?: 'Send returned false'; error_log("Petition email send failed: " . strip_tags($output)); return false; } return true; } catch (\Exception $e) { - // Note: Exception message doesn't contain user data + $errorMessage = $e->getMessage(); error_log("Petition email exception: " . $e->getMessage()); return false; } @@ -750,32 +796,43 @@ function petitionGetSignatureByToken(string $csvPath, string $token): ?array { /** * Send thank you email with delete link (with retry wrapper) */ -function petitionSendThankYouEmail(string $token, string $deleteUrl, string $petitionTitle, string $csvPath, Context $ctx): bool { - return petitionSendWithRetry(function() use ($token, $deleteUrl, $petitionTitle, $csvPath, $ctx) { - return petitionSendThankYouEmailInternal($token, $deleteUrl, $petitionTitle, $csvPath, $ctx); +function petitionSendThankYouEmail(string $token, string $deleteUrl, string $petitionTitle, string $petitionId, string $csvPath, Context $ctx): bool { + $signature = petitionGetSignatureByToken($csvPath, $token); + $email = $signature['email'] ?? 'unknown'; + + $result = petitionSendWithRetry(function() use ($token, $deleteUrl, $petitionTitle, $csvPath, $ctx, &$errorMessage) { + return petitionSendThankYouEmailInternal($token, $deleteUrl, $petitionTitle, $csvPath, $ctx, $errorMessage); }); + + // Log the final result + petitionLogEmail('thankyou', $petitionId, $email, $result, $result ? '' : ($errorMessage ?? 'Unknown error')); + + return $result; } /** * Internal: Send thank you email with delete link (single attempt) */ -function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, string $petitionTitle, string $csvPath, Context $ctx): bool { +function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, string $petitionTitle, string $csvPath, Context $ctx, ?string &$errorMessage = null): bool { // Decode HTML entities for plain-text email (e.g., – → –) $petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $signature = petitionGetSignatureByToken($csvPath, $token); if (!$signature) { + $errorMessage = 'Signature not found'; return false; } $smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php'; if (!file_exists($smtpConfigPath)) { + $errorMessage = 'SMTP config not found'; return false; } $config = require $smtpConfigPath; if (!$config['enabled']) { + $errorMessage = 'SMTP disabled'; return false; } @@ -805,6 +862,7 @@ function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, str // Pre-flight check $fp = @fsockopen($config['host'], $config['port'], $errno, $errstr, 10); if (!$fp) { + $errorMessage = "Connection failed: {$errno} - {$errstr}"; error_log("Petition SMTP pre-flight failed: {$config['host']}:{$config['port']} - {$errno} - {$errstr}"); return false; } @@ -831,12 +889,14 @@ function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, str $output = ob_get_clean(); if (!$sent || stripos($output, 'error') !== false || stripos($output, '✗') !== false) { + $errorMessage = strip_tags($output) ?: 'Send returned false'; error_log("Petition thank you email send failed: " . strip_tags($output)); return false; } return true; } catch (\Exception $e) { + $errorMessage = $e->getMessage(); error_log("Petition thank you email exception: " . $e->getMessage()); return false; } @@ -1096,7 +1156,7 @@ function petitionGetPageData(?Context $ctx): ?array { $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST']; $deleteUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?delete={$token}#sign-now"; - petitionSendThankYouEmail($token, $deleteUrl, $petitionTitle, $csvPath, $ctx); + petitionSendThankYouEmail($token, $deleteUrl, $petitionTitle, $petitionId, $csvPath, $ctx); break; case 'already': $confirmMessage = ['type' => 'info', 'text' => petitionT($ctx, 'petition', 'confirm_already')]; @@ -1245,7 +1305,7 @@ function petitionGetPageData(?Context $ctx): ?array { $confirmUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?confirm={$token}#sign-now"; // Send confirmation email - if (petitionSendConfirmationEmail($signatureData, $confirmUrl, $petitionTitle, $ctx)) { + if (petitionSendConfirmationEmail($signatureData, $confirmUrl, $petitionTitle, $petitionId, $ctx)) { // Subscribe to newsletter if opted in (fire and forget - don't block petition) $newsletterOptIn = isset($_POST['newsletter']) && $_POST['newsletter'] === 'on'; error_log("Newsletter checkbox: " . ($newsletterOptIn ? 'checked' : 'not checked')); From 7659b0cf5ea386d725f8786ba25df963c71bc790 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 22 Jan 2026 20:54:31 +0100 Subject: [PATCH 2/2] Create petition-cli.php --- custom/petition-cli.php | 583 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100755 custom/petition-cli.php diff --git a/custom/petition-cli.php b/custom/petition-cli.php new file mode 100755 index 0000000..83746b2 --- /dev/null +++ b/custom/petition-cli.php @@ -0,0 +1,583 @@ +#!/usr/bin/env php + (int)$row[0], + 'type' => $row[1], + 'petition_id' => $row[2], + 'email' => $row[3], + 'error' => $row[5] ?? '' + ]; + } + } + + fclose($fp); + + // Sort by timestamp descending (newest first) + usort($failed, fn($a, $b) => $b['timestamp'] - $a['timestamp']); + + // Remove duplicates (keep only latest failure per email+type+petition) + $unique = []; + $seen = []; + foreach ($failed as $entry) { + $key = $entry['email'] . '|' . $entry['type'] . '|' . $entry['petition_id']; + if (!isset($seen[$key])) { + $seen[$key] = true; + $unique[] = $entry; + } + } + + return $unique; +} + +/** + * Get unconfirmed signatures from a petition CSV + */ +function getUnconfirmedSignatures(string $petitionId): array { + $csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv'; + if (!file_exists($csvPath)) { + return []; + } + + $unconfirmed = []; + $fp = fopen($csvPath, 'r'); + if (!$fp) { + return []; + } + + // Skip header + fgetcsv($fp, null, ',', '"', ''); + + // CSV format: timestamp, email, firstname, surname, region, display, status, token, token_created, ip_hash + while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { + if (isset($row[6]) && $row[6] === 'pending') { + $tokenCreated = isset($row[8]) ? (int)$row[8] : (int)$row[0]; + $unconfirmed[] = [ + 'timestamp' => (int)$row[0], + 'email' => $row[1], + 'firstname' => $row[2], + 'surname' => $row[3], + 'region' => $row[4], + 'display' => $row[5], + 'token' => $row[7], + 'token_created' => $tokenCreated, + 'petition_id' => $petitionId + ]; + } + } + + fclose($fp); + + // Sort by timestamp descending + usort($unconfirmed, fn($a, $b) => $b['timestamp'] - $a['timestamp']); + + return $unconfirmed; +} + +/** + * Get all unconfirmed signatures from all petitions + */ +function getAllUnconfirmedSignatures(): array { + $all = []; + foreach (getPetitionFiles() as $petitionId) { + $all = array_merge($all, getUnconfirmedSignatures($petitionId)); + } + + // Sort by timestamp descending + usort($all, fn($a, $b) => $b['timestamp'] - $a['timestamp']); + + return $all; +} + +/** + * Send a single email using PHPMailer + */ +function sendEmail(string $to, string $toName, string $subject, string $body): array { + $config = loadSmtpConfig(); + if (!$config || !$config['enabled']) { + return ['success' => false, 'error' => 'SMTP ikke konfigurert eller deaktivert']; + } + + $fromEmail = $config['petition']['from_email'] ?? $config['from_email']; + $fromName = $config['petition']['from_name'] ?? $config['from_name']; + + // Pre-flight check + $fp = @fsockopen($config['host'], $config['port'], $errno, $errstr, 10); + if (!$fp) { + return ['success' => false, 'error' => "Tilkobling feilet: {$errno} - {$errstr}"]; + } + fclose($fp); + + try { + require_once BASE_DIR . '/vendor/PHPMailer.Lite.php'; + + $mail = new \codeworxtech\PHPMailerLite\PHPMailerLite(); + + $mail->SetSMTPhost($config['host']); + $mail->SetSMTPport($config['port']); + $mail->SetSMTPuser($config['username']); + $mail->SetSMTPpass($config['password']); + + $mail->SetSender([$fromEmail => $fromName]); + $mail->AddRecipient([$to => $toName]); + $mail->SetSubject($subject); + $mail->SetBodyText($body); + + ob_start(); + $sent = @$mail->Send('smtp'); + $output = ob_get_clean(); + + if (!$sent || stripos($output, 'error') !== false || stripos($output, '✗') !== false) { + return ['success' => false, 'error' => strip_tags($output) ?: 'Sending feilet']; + } + + return ['success' => true, 'error' => '']; + } catch (\Exception $e) { + return ['success' => false, 'error' => $e->getMessage()]; + } +} + +/** + * Build confirmation email body + */ +function buildConfirmationEmail(array $signature, string $petitionTitle): array { + $confirmUrl = "https://stopplidelsen.no/underskriftskampanjer/{$signature['petition_id']}/?confirm={$signature['token']}#sign-now"; + + $body = "Hei {$signature['firstname']}!\n\n"; + $body .= "Takk for at du signerte underskriftskampanjen \"{$petitionTitle}\".\n\n"; + $body .= "Klikk lenken under for a bekrefte signaturen din:\n"; + $body .= "{$confirmUrl}\n\n"; + $body .= "Lenken utloper om 30 dager.\n\n"; + $body .= "Hvis du ikke signerte denne kampanjen, kan du trygt ignorere denne e-posten.\n\n"; + $body .= "---\n\n"; + $body .= "Du kan trekke tilbake signaturen din nar som helst ved a folge lenken i bekreftelsese-posten.\n\n"; + $body .= "Med vennlig hilsen,\n"; + $body .= "Stopp lidelsen\n"; + + return [ + 'subject' => 'Bekreft signaturen din', + 'body' => $body + ]; +} + +/** + * Log email result to SMTP log + */ +function logEmailResult(string $type, string $petitionId, string $email, bool $success, string $error = ''): void { + $fp = fopen(SMTP_LOG, 'a'); + if (!$fp) { + return; + } + + if (flock($fp, LOCK_EX)) { + fputcsv($fp, [ + time(), + $type, + $petitionId, + $email, + $success ? 'success' : 'failed', + $error + ], ',', '"', ''); + flock($fp, LOCK_UN); + } + + fclose($fp); +} + +/** + * Format timestamp for display + */ +function formatDate(int $timestamp): string { + return date('Y-m-d H:i', $timestamp); +} + +/** + * Print colored output + */ +function printColor(string $text, string $color): void { + $colors = [ + 'red' => "\033[31m", + 'green' => "\033[32m", + 'yellow' => "\033[33m", + 'blue' => "\033[34m", + 'reset' => "\033[0m" + ]; + + echo ($colors[$color] ?? '') . $text . $colors['reset']; +} + +/** + * Read user input + */ +function prompt(string $message): string { + echo $message; + return trim(fgets(STDIN)); +} + +/** + * Confirm action + */ +function confirm(string $message): bool { + $response = strtolower(prompt($message . ' [j/n]: ')); + return $response === 'j' || $response === 'ja' || $response === 'y' || $response === 'yes'; +} + +/** + * Display main menu + */ +function showMenu(): void { + echo "\n"; + printColor("=== Underskriftskampanje E-post CLI ===\n", 'blue'); + echo "\n"; + echo "Velg handling:\n"; + echo " 1) Vis mislykkede e-poster fra SMTP-logg\n"; + echo " 2) Vis ubekreftede signaturer\n"; + echo " 3) Send e-post pa nytt til mislykkede\n"; + echo " 4) Send bekreftelse pa nytt til ubekreftede\n"; + echo " 5) Avslutt\n"; + echo "\n"; +} + +/** + * List failed emails + */ +function listFailedEmails(): void { + $failed = getFailedEmails(); + + if (empty($failed)) { + printColor("\nIngen mislykkede e-poster i loggen.\n", 'green'); + return; + } + + echo "\n"; + printColor("Mislykkede e-poster (" . count($failed) . " stk):\n", 'yellow'); + echo str_repeat('-', 80) . "\n"; + + foreach ($failed as $i => $entry) { + $num = $i + 1; + $date = formatDate($entry['timestamp']); + $type = $entry['type'] === 'confirmation' ? 'Bekreftelse' : 'Takk'; + echo sprintf( + "%3d. %-35s %-12s %-20s\n Feil: %s\n", + $num, + $entry['email'], + $type, + $date, + $entry['error'] ?: '(ukjent)' + ); + } + echo str_repeat('-', 80) . "\n"; +} + +/** + * List unconfirmed signatures + */ +function listUnconfirmedSignatures(): void { + $unconfirmed = getAllUnconfirmedSignatures(); + + if (empty($unconfirmed)) { + printColor("\nIngen ubekreftede signaturer.\n", 'green'); + return; + } + + // Filter out expired tokens (older than 30 days) + $currentTime = time(); + $valid = array_filter($unconfirmed, function($sig) use ($currentTime) { + return ($currentTime - $sig['token_created']) <= 2592000; // 30 days + }); + + $expired = count($unconfirmed) - count($valid); + + echo "\n"; + printColor("Ubekreftede signaturer (" . count($valid) . " gyldige", 'yellow'); + if ($expired > 0) { + echo ", {$expired} utlopte"; + } + echo "):\n"; + echo str_repeat('-', 80) . "\n"; + + foreach ($valid as $i => $sig) { + $num = $i + 1; + $date = formatDate($sig['timestamp']); + $name = $sig['firstname'] . ' ' . $sig['surname']; + $daysLeft = 30 - floor(($currentTime - $sig['token_created']) / 86400); + + echo sprintf( + "%3d. %-35s %-20s\n %s (%d dager igjen)\n", + $num, + $sig['email'], + $name, + $date, + $daysLeft + ); + } + echo str_repeat('-', 80) . "\n"; +} + +/** + * Retry sending failed emails + */ +function retryFailedEmails(): void { + $failed = getFailedEmails(); + + if (empty($failed)) { + printColor("\nIngen mislykkede e-poster a sende pa nytt.\n", 'green'); + return; + } + + listFailedEmails(); + + if (!confirm("\nSende pa nytt til alle " . count($failed) . " adresser?")) { + echo "Avbrutt.\n"; + return; + } + + $total = count($failed); + $estimatedMinutes = ceil(($total * EMAIL_DELAY_SECONDS) / 60); + + echo "\n"; + printColor("Sender med " . EMAIL_DELAY_SECONDS . " sekunders mellomrom (maks 250/time)...\n", 'blue'); + echo "Estimert tid: ca. {$estimatedMinutes} minutter\n\n"; + + $success = 0; + $failures = 0; + + foreach ($failed as $i => $entry) { + $num = $i + 1; + echo "[{$num}/{$total}] {$entry['email']} ... "; + + // For now, we can only retry confirmation emails properly + // Thank you emails need more context that we don't have + if ($entry['type'] !== 'confirmation') { + printColor("HOPPET OVER (type: {$entry['type']})\n", 'yellow'); + continue; + } + + // We need to get the signature data from the petition CSV + $signatures = getUnconfirmedSignatures($entry['petition_id']); + $signature = null; + foreach ($signatures as $sig) { + if (strtolower($sig['email']) === strtolower($entry['email'])) { + $signature = $sig; + break; + } + } + + if (!$signature) { + printColor("HOPPET OVER (signatur ikke funnet eller allerede bekreftet)\n", 'yellow'); + continue; + } + + // Check token expiry + if ((time() - $signature['token_created']) > 2592000) { + printColor("HOPPET OVER (token utlopt)\n", 'yellow'); + continue; + } + + $email = buildConfirmationEmail($signature, $entry['petition_id']); + $result = sendEmail( + $signature['email'], + $signature['firstname'] . ' ' . $signature['surname'], + $email['subject'], + $email['body'] + ); + + if ($result['success']) { + printColor("OK\n", 'green'); + logEmailResult('confirmation', $entry['petition_id'], $signature['email'], true); + $success++; + } else { + printColor("FEILET: {$result['error']}\n", 'red'); + logEmailResult('confirmation', $entry['petition_id'], $signature['email'], false, $result['error']); + $failures++; + } + + // Wait before next email (except for last one) + if ($i < $total - 1) { + sleep(EMAIL_DELAY_SECONDS); + } + } + + echo "\n"; + printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green'); +} + +/** + * Resend confirmation to unconfirmed signatures + */ +function resendToUnconfirmed(): void { + $unconfirmed = getAllUnconfirmedSignatures(); + + // Filter out expired tokens + $currentTime = time(); + $valid = array_filter($unconfirmed, function($sig) use ($currentTime) { + return ($currentTime - $sig['token_created']) <= 2592000; + }); + $valid = array_values($valid); // Re-index + + if (empty($valid)) { + printColor("\nIngen gyldige ubekreftede signaturer a sende til.\n", 'green'); + return; + } + + listUnconfirmedSignatures(); + + if (!confirm("\nSende bekreftelse pa nytt til alle " . count($valid) . " adresser?")) { + echo "Avbrutt.\n"; + return; + } + + $total = count($valid); + $estimatedMinutes = ceil(($total * EMAIL_DELAY_SECONDS) / 60); + + echo "\n"; + printColor("Sender med " . EMAIL_DELAY_SECONDS . " sekunders mellomrom (maks 250/time)...\n", 'blue'); + echo "Estimert tid: ca. {$estimatedMinutes} minutter\n\n"; + + $success = 0; + $failures = 0; + + foreach ($valid as $i => $signature) { + $num = $i + 1; + echo "[{$num}/{$total}] {$signature['email']} ... "; + + $email = buildConfirmationEmail($signature, $signature['petition_id']); + $result = sendEmail( + $signature['email'], + $signature['firstname'] . ' ' . $signature['surname'], + $email['subject'], + $email['body'] + ); + + if ($result['success']) { + printColor("OK\n", 'green'); + logEmailResult('confirmation', $signature['petition_id'], $signature['email'], true); + $success++; + } else { + printColor("FEILET: {$result['error']}\n", 'red'); + logEmailResult('confirmation', $signature['petition_id'], $signature['email'], false, $result['error']); + $failures++; + } + + // Wait before next email (except for last one) + if ($i < $total - 1) { + sleep(EMAIL_DELAY_SECONDS); + } + } + + echo "\n"; + printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green'); +} + +// Main loop +function main(): void { + // Check requirements + $config = loadSmtpConfig(); + if (!$config) { + printColor("Feil: SMTP-konfigurasjon ikke funnet (smtp-config.php)\n", 'red'); + exit(1); + } + + if (!$config['enabled']) { + printColor("Advarsel: SMTP er deaktivert i konfigurasjonen\n", 'yellow'); + } + + while (true) { + showMenu(); + $choice = prompt('Valg: '); + + switch ($choice) { + case '1': + listFailedEmails(); + break; + case '2': + listUnconfirmedSignatures(); + break; + case '3': + retryFailedEmails(); + break; + case '4': + resendToUnconfirmed(); + break; + case '5': + case 'q': + case 'quit': + case 'exit': + echo "Ha det!\n"; + exit(0); + default: + printColor("Ugyldig valg. Velg 1-5.\n", 'red'); + } + } +} + +// Run +main();