From 8f49bb17cd4500f37e08e228d6be24a4cf0f269d Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 22 Jan 2026 20:54:28 +0100 Subject: [PATCH] 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'));