Merge branch 'latest' of forge.dmz.skyfritt.net:stopplidelsen/innhold into latest
This commit is contained in:
commit
7972a4c5ce
2 changed files with 656 additions and 13 deletions
583
custom/petition-cli.php
Executable file
583
custom/petition-cli.php
Executable file
|
|
@ -0,0 +1,583 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Petition Email CLI Tool
|
||||
*
|
||||
* Interactive tool for managing petition emails:
|
||||
* - View failed email sends from SMTP log
|
||||
* - View unconfirmed signatures from petition CSVs
|
||||
* - Retry sending emails with rate limiting (250/hour max)
|
||||
*
|
||||
* Usage: php custom/petition-cli.php
|
||||
*/
|
||||
|
||||
// Ensure CLI only
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("This script can only be run from the command line.\n");
|
||||
}
|
||||
|
||||
define('BASE_DIR', __DIR__);
|
||||
define('DATA_DIR', BASE_DIR . '/data');
|
||||
define('PETITIONS_DIR', DATA_DIR . '/petitions');
|
||||
define('SMTP_LOG', DATA_DIR . '/smtp-log.csv');
|
||||
|
||||
// Rate limit: 250 emails/hour = 1 email per 14.4 seconds, using 15 seconds
|
||||
define('EMAIL_DELAY_SECONDS', 15);
|
||||
|
||||
/**
|
||||
* Load SMTP configuration
|
||||
*/
|
||||
function loadSmtpConfig(): ?array {
|
||||
$configPath = BASE_DIR . '/smtp-config.php';
|
||||
if (!file_exists($configPath)) {
|
||||
return null;
|
||||
}
|
||||
return require $configPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all petition CSV files
|
||||
*/
|
||||
function getPetitionFiles(): array {
|
||||
if (!is_dir(PETITIONS_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = glob(PETITIONS_DIR . '/*.csv');
|
||||
return array_map(function($f) {
|
||||
return basename($f, '.csv');
|
||||
}, $files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read failed emails from SMTP log
|
||||
*/
|
||||
function getFailedEmails(): array {
|
||||
if (!file_exists(SMTP_LOG)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$failed = [];
|
||||
$fp = fopen(SMTP_LOG, 'r');
|
||||
if (!$fp) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Skip header
|
||||
fgetcsv($fp, null, ',', '"', '');
|
||||
|
||||
// CSV format: timestamp, type, petition_id, email, status, error_message
|
||||
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
|
||||
if (isset($row[4]) && $row[4] === 'failed') {
|
||||
$failed[] = [
|
||||
'timestamp' => (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();
|
||||
|
|
@ -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'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue