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