#!/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();