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
This commit is contained in:
Ruben 2026-01-22 20:54:28 +01:00
parent b79bcd9fe9
commit 8f49bb17cd

View file

@ -584,6 +584,45 @@ function petitionFormatDate(int $timestamp, Context $ctx): string {
return "{$day}. {$month} {$year}, {$time}"; 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 * Retry wrapper for email sending with exponential backoff
* Retries up to maxRetries times with increasing delays * 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) * Send confirmation email (with retry wrapper)
*/ */
function petitionSendConfirmationEmail(array $data, string $confirmUrl, string $petitionTitle, Context $ctx): bool { function petitionSendConfirmationEmail(array $data, string $confirmUrl, string $petitionTitle, string $petitionId, Context $ctx): bool {
return petitionSendWithRetry(function() use ($data, $confirmUrl, $petitionTitle, $ctx) { $result = petitionSendWithRetry(function() use ($data, $confirmUrl, $petitionTitle, $ctx, &$errorMessage) {
return petitionSendConfirmationEmailInternal($data, $confirmUrl, $petitionTitle, $ctx); 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) * 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., – → ) // Decode HTML entities for plain-text email (e.g., – → )
$petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php'; $smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php';
if (!file_exists($smtpConfigPath)) { if (!file_exists($smtpConfigPath)) {
$errorMessage = 'SMTP config not found';
return false; return false;
} }
$config = require $smtpConfigPath; $config = require $smtpConfigPath;
if (!$config['enabled']) { if (!$config['enabled']) {
$errorMessage = 'SMTP disabled';
return false; return false;
} }
@ -670,7 +716,7 @@ function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl,
// Pre-flight check // Pre-flight check
$fp = @fsockopen($config['host'], $config['port'], $errno, $errstr, 10); $fp = @fsockopen($config['host'], $config['port'], $errno, $errstr, 10);
if (!$fp) { 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}"); error_log("Petition SMTP pre-flight failed: {$config['host']}:{$config['port']} - {$errno} - {$errstr}");
return false; return false;
} }
@ -697,14 +743,14 @@ function petitionSendConfirmationEmailInternal(array $data, string $confirmUrl,
$output = ob_get_clean(); $output = ob_get_clean();
if (!$sent || stripos($output, 'error') !== false || stripos($output, '✗') !== false) { 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)); error_log("Petition email send failed: " . strip_tags($output));
return false; return false;
} }
return true; return true;
} catch (\Exception $e) { } catch (\Exception $e) {
// Note: Exception message doesn't contain user data $errorMessage = $e->getMessage();
error_log("Petition email exception: " . $e->getMessage()); error_log("Petition email exception: " . $e->getMessage());
return false; return false;
} }
@ -750,32 +796,43 @@ function petitionGetSignatureByToken(string $csvPath, string $token): ?array {
/** /**
* Send thank you email with delete link (with retry wrapper) * Send thank you email with delete link (with retry wrapper)
*/ */
function petitionSendThankYouEmail(string $token, string $deleteUrl, string $petitionTitle, string $csvPath, Context $ctx): bool { function petitionSendThankYouEmail(string $token, string $deleteUrl, string $petitionTitle, string $petitionId, string $csvPath, Context $ctx): bool {
return petitionSendWithRetry(function() use ($token, $deleteUrl, $petitionTitle, $csvPath, $ctx) { $signature = petitionGetSignatureByToken($csvPath, $token);
return petitionSendThankYouEmailInternal($token, $deleteUrl, $petitionTitle, $csvPath, $ctx); $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) * 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., – → ) // Decode HTML entities for plain-text email (e.g., – → )
$petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $petitionTitle = html_entity_decode($petitionTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$signature = petitionGetSignatureByToken($csvPath, $token); $signature = petitionGetSignatureByToken($csvPath, $token);
if (!$signature) { if (!$signature) {
$errorMessage = 'Signature not found';
return false; return false;
} }
$smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php'; $smtpConfigPath = dirname(__DIR__, 2) . '/smtp-config.php';
if (!file_exists($smtpConfigPath)) { if (!file_exists($smtpConfigPath)) {
$errorMessage = 'SMTP config not found';
return false; return false;
} }
$config = require $smtpConfigPath; $config = require $smtpConfigPath;
if (!$config['enabled']) { if (!$config['enabled']) {
$errorMessage = 'SMTP disabled';
return false; return false;
} }
@ -805,6 +862,7 @@ function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, str
// Pre-flight check // Pre-flight check
$fp = @fsockopen($config['host'], $config['port'], $errno, $errstr, 10); $fp = @fsockopen($config['host'], $config['port'], $errno, $errstr, 10);
if (!$fp) { if (!$fp) {
$errorMessage = "Connection failed: {$errno} - {$errstr}";
error_log("Petition SMTP pre-flight failed: {$config['host']}:{$config['port']} - {$errno} - {$errstr}"); error_log("Petition SMTP pre-flight failed: {$config['host']}:{$config['port']} - {$errno} - {$errstr}");
return false; return false;
} }
@ -831,12 +889,14 @@ function petitionSendThankYouEmailInternal(string $token, string $deleteUrl, str
$output = ob_get_clean(); $output = ob_get_clean();
if (!$sent || stripos($output, 'error') !== false || stripos($output, '✗') !== false) { 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)); error_log("Petition thank you email send failed: " . strip_tags($output));
return false; return false;
} }
return true; return true;
} catch (\Exception $e) { } catch (\Exception $e) {
$errorMessage = $e->getMessage();
error_log("Petition thank you email exception: " . $e->getMessage()); error_log("Petition thank you email exception: " . $e->getMessage());
return false; return false;
} }
@ -1096,7 +1156,7 @@ function petitionGetPageData(?Context $ctx): ?array {
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST']; $host = $_SERVER['HTTP_HOST'];
$deleteUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?delete={$token}#sign-now"; $deleteUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?delete={$token}#sign-now";
petitionSendThankYouEmail($token, $deleteUrl, $petitionTitle, $csvPath, $ctx); petitionSendThankYouEmail($token, $deleteUrl, $petitionTitle, $petitionId, $csvPath, $ctx);
break; break;
case 'already': case 'already':
$confirmMessage = ['type' => 'info', 'text' => petitionT($ctx, 'petition', 'confirm_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"; $confirmUrl = "{$protocol}://{$host}{$langPrefix}/{$currentPath}/?confirm={$token}#sign-now";
// Send confirmation email // 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) // Subscribe to newsletter if opted in (fire and forget - don't block petition)
$newsletterOptIn = isset($_POST['newsletter']) && $_POST['newsletter'] === 'on'; $newsletterOptIn = isset($_POST['newsletter']) && $_POST['newsletter'] === 'on';
error_log("Newsletter checkbox: " . ($newsletterOptIn ? 'checked' : 'not checked')); error_log("Newsletter checkbox: " . ($newsletterOptIn ? 'checked' : 'not checked'));