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'));