#!/usr/bin/env php (int)$row[0], 'type' => $row[1], 'petition_id' => $row[2], 'email' => $row[3], 'reason' => $row[4] ?? '' ]; } } fclose($fp); return $ignored; } /** * Add entry to ignore list */ function addToIgnoreList(string $type, string $petitionId, string $email, string $reason): bool { $isNewFile = !file_exists(IGNORE_LIST); $fp = fopen(IGNORE_LIST, 'a'); if (!$fp) { return false; } if (flock($fp, LOCK_EX)) { if ($isNewFile) { fputcsv($fp, ['timestamp', 'type', 'petition_id', 'email', 'reason'], ',', '"', ''); } fputcsv($fp, [ time(), $type, $petitionId, $email, $reason ], ',', '"', ''); flock($fp, LOCK_UN); fclose($fp); return true; } fclose($fp); return false; } /** * Check if entry is ignored */ function isIgnored(array $ignoreList, string $type, string $petitionId, string $email): bool { $key = $type . '|' . $petitionId . '|' . strtolower($email); return isset($ignoreList[$key]); } /** * Read failed emails from SMTP log */ function getFailedEmails(bool $excludeIgnored = true): array { if (!file_exists(SMTP_LOG)) { return []; } $ignoreList = $excludeIgnored ? loadIgnoreList() : []; $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') { $entry = [ 'timestamp' => (int)$row[0], 'type' => $row[1], 'petition_id' => $row[2], 'email' => $row[3], 'error' => $row[5] ?? '' ]; // Skip if ignored if ($excludeIgnored && isIgnored($ignoreList, $entry['type'], $entry['petition_id'], $entry['email'])) { continue; } $failed[] = $entry; } } 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, bool $excludeIgnored = true): array { $csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv'; if (!file_exists($csvPath)) { return []; } $ignoreList = $excludeIgnored ? loadIgnoreList() : []; $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]; $entry = [ '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 ]; // Skip if ignored if ($excludeIgnored && isIgnored($ignoreList, 'unconfirmed', $petitionId, $entry['email'])) { continue; } $unconfirmed[] = $entry; } } 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(bool $excludeIgnored = true): array { $all = []; foreach (getPetitionFiles() as $petitionId) { $all = array_merge($all, getUnconfirmedSignatures($petitionId, $excludeIgnored)); } // 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']; // Get petition-specific SMTP settings (allows separate SMTP account for better deliverability) $smtpHost = $config['petition']['host'] ?? $config['host']; $smtpPort = $config['petition']['port'] ?? $config['port']; $smtpUser = $config['petition']['username'] ?? $config['username']; $smtpPass = $config['petition']['password'] ?? $config['password']; // Pre-flight check $fp = @fsockopen($smtpHost, $smtpPort, $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($smtpHost); $mail->SetSMTPport($smtpPort); $mail->SetSMTPuser($smtpUser); $mail->SetSMTPpass($smtpPass); $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/underskriftskampanje/{$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", 'cyan' => "\033[36m", '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) Send bekreftelse til spesifikke e-postadresser\n"; echo " 6) Marker oppforinger som ignorert\n"; echo " 7) Manuell bekreftelse av signaturer\n"; echo " 8) Slett signaturer\n"; echo " 9) 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; } // Ask for custom delay $delaySeconds = EMAIL_DELAY_SECONDS; $customDelay = prompt("\nForsinkelse mellom e-poster i sekunder [standard: " . EMAIL_DELAY_SECONDS . "]: "); if (!empty($customDelay) && is_numeric($customDelay) && (int)$customDelay >= 0) { $delaySeconds = (int)$customDelay; } $total = count($valid); $estimatedMinutes = $delaySeconds > 0 ? ceil(($total * $delaySeconds) / 60) : 0; echo "\n"; printColor("Sender med " . $delaySeconds . " sekunders mellomrom...\n", 'blue'); if ($estimatedMinutes > 0) { echo "Estimert tid: ca. {$estimatedMinutes} minutter\n\n"; } else { echo "\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 && $delaySeconds > 0) { sleep($delaySeconds); } } echo "\n"; printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green'); } /** * Send confirmation to specific email addresses */ function sendToSpecificEmails(): void { echo "\n"; printColor("Send bekreftelse til spesifikke e-postadresser\n", 'cyan'); echo "\n"; $input = prompt("Skriv inn e-postadresse(r) (kommaseparert): "); if (empty(trim($input))) { echo "Ingen e-postadresser oppgitt.\n"; return; } // Parse and clean email addresses $emails = array_map('trim', explode(',', $input)); $emails = array_filter($emails, fn($e) => !empty($e)); $emails = array_map('strtolower', $emails); if (empty($emails)) { echo "Ingen gyldige e-postadresser oppgitt.\n"; return; } echo "\n"; echo "Soker etter " . count($emails) . " e-postadresse(r)...\n\n"; // Find matching unconfirmed signatures $found = []; $currentTime = time(); foreach (getPetitionFiles() as $petitionId) { $signatures = getUnconfirmedSignatures($petitionId, false); // Include ignored foreach ($signatures as $sig) { if (in_array(strtolower($sig['email']), $emails)) { $sig['expired'] = ($currentTime - $sig['token_created']) > 2592000; $found[] = $sig; } } } // Determine which emails were not found $foundEmails = array_map(fn($f) => strtolower($f['email']), $found); $notFound = []; foreach ($emails as $email) { if (!in_array($email, $foundEmails)) { $notFound[] = $email; } } if (!empty($notFound)) { printColor("Ikke funnet (eller allerede bekreftet):\n", 'red'); foreach ($notFound as $email) { echo " - {$email}\n"; } echo "\n"; } if (empty($found)) { echo "Ingen ubekreftede signaturer a sende til.\n"; return; } // Show what will be sent printColor("Fant folgende ubekreftede signaturer:\n", 'yellow'); foreach ($found as $entry) { $date = formatDate($entry['timestamp']); $name = $entry['firstname'] . ' ' . $entry['surname']; $status = $entry['expired'] ? ' [UTLOPT]' : ''; echo " - {$entry['email']} ({$name}) - {$entry['petition_id']}{$status}\n"; } // Filter out expired $valid = array_filter($found, fn($f) => !$f['expired']); $valid = array_values($valid); $expiredCount = count($found) - count($valid); if ($expiredCount > 0) { echo "\n"; printColor("{$expiredCount} signatur(er) har utlopt token og vil bli hoppet over.\n", 'yellow'); } if (empty($valid)) { echo "Ingen gyldige signaturer a sende til.\n"; return; } echo "\n"; if (!confirm("Sende bekreftelse til " . count($valid) . " adresse(r)?")) { echo "Avbrutt.\n"; return; } // Ask for custom delay $delaySeconds = EMAIL_DELAY_SECONDS; $customDelay = prompt("\nForsinkelse mellom e-poster i sekunder [standard: " . EMAIL_DELAY_SECONDS . "]: "); if (!empty($customDelay) && is_numeric($customDelay) && (int)$customDelay >= 0) { $delaySeconds = (int)$customDelay; } $total = count($valid); echo "\n"; printColor("Sender...\n", 'blue'); echo "\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 && $delaySeconds > 0) { sleep($delaySeconds); } } echo "\n"; printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green'); } /** * Atomically modify petition CSV file with a callback function. * The callback receives the rows array (without header) by reference and can modify it. * Returns the result from the callback, or null on file errors. * * @param string $petitionId The petition ID * @param callable $callback Function that receives (&$rows, $petitionId) and returns a result * @return mixed|null The callback's return value, or null on error */ function modifyPetitionFile(string $petitionId, callable $callback): mixed { $csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv'; if (!file_exists($csvPath)) { return null; } // Open with r+ to allow reading and writing, acquire exclusive lock immediately // This matches the pattern used in petition-form.php to prevent race conditions $fp = fopen($csvPath, 'r+'); if (!$fp) { printColor(" Advarsel: Kunne ikke apne {$petitionId}.csv\n", 'yellow'); return null; } if (!flock($fp, LOCK_EX)) { fclose($fp); printColor(" Advarsel: Kunne ikke lase {$petitionId}.csv\n", 'yellow'); return null; } // Read all rows while holding the lock $header = fgetcsv($fp, null, ',', '"', ''); if ($header === false) { flock($fp, LOCK_UN); fclose($fp); return null; } $rows = []; while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { $rows[] = $row; } // Call the callback to modify rows $originalCount = count($rows); $result = $callback($rows, $petitionId); // Write back if rows were modified (count changed or callback signals modification) $modified = (count($rows) !== $originalCount); if (!$modified && is_array($result) && isset($result['modified'])) { $modified = $result['modified']; } if ($modified) { rewind($fp); ftruncate($fp, 0); fputcsv($fp, $header, ',', '"', ''); foreach ($rows as $row) { fputcsv($fp, $row, ',', '"', ''); } } flock($fp, LOCK_UN); fclose($fp); return $result; } /** * Manually confirm signatures by email address */ function manuallyConfirmSignatures(): void { echo "\n"; printColor("Manuell bekreftelse av signaturer\n", 'cyan'); echo "\n"; $input = prompt("Skriv inn e-postadresse(r) (kommaseparert): "); if (empty(trim($input))) { echo "Ingen e-postadresser oppgitt.\n"; return; } // Parse and clean email addresses $emails = array_map('trim', explode(',', $input)); $emails = array_filter($emails, fn($e) => !empty($e)); $emails = array_map('strtolower', $emails); if (empty($emails)) { echo "Ingen gyldige e-postadresser oppgitt.\n"; return; } echo "\n"; echo "Soker etter " . count($emails) . " e-postadresse(r)...\n\n"; $found = []; // Search through all petition files foreach (getPetitionFiles() as $petitionId) { $result = modifyPetitionFile($petitionId, function(&$rows, $petitionId) use ($emails) { $found = []; $modified = false; foreach ($rows as &$row) { if (!isset($row[1]) || !isset($row[6])) { continue; } $rowEmail = strtolower($row[1]); if (in_array($rowEmail, $emails)) { if ($row[6] === 'pending') { $row[6] = 'confirmed'; $modified = true; $found[] = [ 'email' => $row[1], 'name' => ($row[2] ?? '') . ' ' . ($row[3] ?? ''), 'petition_id' => $petitionId, 'was_pending' => true ]; } else { $found[] = [ 'email' => $row[1], 'name' => ($row[2] ?? '') . ' ' . ($row[3] ?? ''), 'petition_id' => $petitionId, 'was_pending' => false, 'status' => $row[6] ]; } } } unset($row); return ['found' => $found, 'modified' => $modified]; }); if ($result && !empty($result['found'])) { $found = array_merge($found, $result['found']); } } // Determine which emails were not found $foundEmails = array_map(fn($f) => strtolower($f['email']), $found); $notFound = []; foreach ($emails as $email) { if (!in_array($email, $foundEmails)) { $notFound[] = $email; } } // Display results if (!empty($found)) { printColor("Funnet:\n", 'green'); foreach ($found as $entry) { $status = $entry['was_pending'] ? "BEKREFTET" : "allerede " . $entry['status']; echo " - {$entry['email']} ({$entry['name']}) - {$entry['petition_id']}: "; if ($entry['was_pending']) { printColor("{$status}\n", 'green'); } else { printColor("{$status}\n", 'yellow'); } } } if (!empty($notFound)) { echo "\n"; printColor("Ikke funnet:\n", 'red'); foreach ($notFound as $email) { echo " - {$email}\n"; } } $confirmedCount = count(array_filter($found, fn($f) => $f['was_pending'])); echo "\n"; printColor("Ferdig: {$confirmedCount} signatur(er) bekreftet\n", 'green'); } /** * Manually delete signatures by email address */ function manuallyDeleteSignatures(): void { echo "\n"; printColor("Slett signaturer\n", 'cyan'); echo "\n"; $input = prompt("Skriv inn e-postadresse(r) (kommaseparert): "); if (empty(trim($input))) { echo "Ingen e-postadresser oppgitt.\n"; return; } // Parse and clean email addresses $emails = array_map('trim', explode(',', $input)); $emails = array_filter($emails, fn($e) => !empty($e)); $emails = array_map('strtolower', $emails); if (empty($emails)) { echo "Ingen gyldige e-postadresser oppgitt.\n"; return; } echo "\n"; echo "Soker etter " . count($emails) . " e-postadresse(r)...\n\n"; $found = []; // First pass: find all matching signatures (read-only) foreach (getPetitionFiles() as $petitionId) { $csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv'; if (!file_exists($csvPath)) { continue; } $fp = fopen($csvPath, 'r'); if (!$fp) { continue; } if (!flock($fp, LOCK_SH)) { fclose($fp); continue; } fgetcsv($fp, null, ',', '"', ''); // Skip header while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { if (!isset($row[1])) { continue; } $rowEmail = strtolower($row[1]); if (in_array($rowEmail, $emails)) { $found[] = [ 'email' => $row[1], 'name' => ($row[2] ?? '') . ' ' . ($row[3] ?? ''), 'petition_id' => $petitionId, 'status' => $row[6] ?? 'unknown', 'timestamp' => (int)($row[0] ?? 0) ]; } } flock($fp, LOCK_UN); fclose($fp); } // Determine which emails were not found $foundEmails = array_map(fn($f) => strtolower($f['email']), $found); $notFound = []; foreach ($emails as $email) { if (!in_array($email, $foundEmails)) { $notFound[] = $email; } } if (!empty($notFound)) { printColor("Ikke funnet:\n", 'red'); foreach ($notFound as $email) { echo " - {$email}\n"; } echo "\n"; } if (empty($found)) { echo "Ingen signaturer a slette.\n"; return; } // Show what will be deleted printColor("Folgende signaturer vil bli slettet:\n", 'yellow'); foreach ($found as $entry) { $date = formatDate($entry['timestamp']); echo " - {$entry['email']} ({$entry['name']}) - {$entry['petition_id']} [{$entry['status']}] {$date}\n"; } echo "\n"; if (!confirm("Er du sikker pa at du vil slette " . count($found) . " signatur(er)?")) { echo "Avbrutt.\n"; return; } // Second pass: delete the signatures $deleted = 0; $emailsToDelete = array_map(fn($f) => strtolower($f['email']), $found); foreach (getPetitionFiles() as $petitionId) { $result = modifyPetitionFile($petitionId, function(&$rows, $petitionId) use ($emailsToDelete) { $deletedCount = 0; $rows = array_filter($rows, function($row) use ($emailsToDelete, &$deletedCount) { if (isset($row[1]) && in_array(strtolower($row[1]), $emailsToDelete)) { $deletedCount++; return false; // Remove this row } return true; // Keep this row }); $rows = array_values($rows); // Re-index return ['deleted' => $deletedCount]; }); if ($result && isset($result['deleted'])) { $deleted += $result['deleted']; } } echo "\n"; printColor("Ferdig: {$deleted} signatur(er) slettet\n", 'green'); } /** * Interactive ignore marking */ function markAsIgnored(): void { echo "\n"; printColor("Marker oppforinger som ignorert\n", 'cyan'); echo "\n"; echo "Velg liste:\n"; echo " 1) Mislykkede e-poster fra SMTP-logg\n"; echo " 2) Ubekreftede signaturer\n"; echo " 3) Tilbake\n"; echo "\n"; $choice = prompt('Valg: '); if ($choice === '1') { markFailedAsIgnored(); } elseif ($choice === '2') { markUnconfirmedAsIgnored(); } } /** * Mark failed emails as ignored (one by one) */ function markFailedAsIgnored(): void { $failed = getFailedEmails(); if (empty($failed)) { printColor("\nIngen mislykkede e-poster a markere.\n", 'green'); return; } echo "\n"; printColor("Ga gjennom mislykkede e-poster en etter en.\n", 'cyan'); echo "Trykk 'j' for a ignorere, 'n' for a beholde, 's' for a hoppe over resten.\n\n"; $ignored = 0; $kept = 0; foreach ($failed as $i => $entry) { $num = $i + 1; $total = count($failed); $date = formatDate($entry['timestamp']); $type = $entry['type'] === 'confirmation' ? 'Bekreftelse' : 'Takk'; echo str_repeat('-', 60) . "\n"; echo "[{$num}/{$total}]\n"; echo " E-post: {$entry['email']}\n"; echo " Type: {$type}\n"; echo " Kampanje: {$entry['petition_id']}\n"; echo " Dato: {$date}\n"; echo " Feil: " . ($entry['error'] ?: '(ukjent)') . "\n"; $response = strtolower(prompt("\nIgnorer denne? [j/n/s]: ")); if ($response === 's' || $response === 'stopp' || $response === 'stop') { echo "Avslutter gjennomgang.\n"; break; } if ($response === 'j' || $response === 'ja' || $response === 'y' || $response === 'yes') { $reason = prompt("Grunn (valgfritt): "); if (addToIgnoreList($entry['type'], $entry['petition_id'], $entry['email'], $reason)) { printColor("Markert som ignorert.\n", 'green'); $ignored++; } else { printColor("Kunne ikke lagre til ignorliste.\n", 'red'); } } else { echo "Beholdt.\n"; $kept++; } } echo "\n"; printColor("Ferdig: {$ignored} ignorert, {$kept} beholdt\n", 'green'); } /** * Mark unconfirmed signatures as ignored (one by one) */ function markUnconfirmedAsIgnored(): void { $unconfirmed = getAllUnconfirmedSignatures(); // Include expired for review $currentTime = time(); if (empty($unconfirmed)) { printColor("\nIngen ubekreftede signaturer a markere.\n", 'green'); return; } echo "\n"; printColor("Ga gjennom ubekreftede signaturer en etter en.\n", 'cyan'); echo "Trykk 'j' for a ignorere, 'n' for a beholde, 's' for a hoppe over resten.\n\n"; $ignored = 0; $kept = 0; foreach ($unconfirmed as $i => $sig) { $num = $i + 1; $total = count($unconfirmed); $date = formatDate($sig['timestamp']); $name = $sig['firstname'] . ' ' . $sig['surname']; $daysLeft = 30 - floor(($currentTime - $sig['token_created']) / 86400); $expired = $daysLeft < 0; echo str_repeat('-', 60) . "\n"; echo "[{$num}/{$total}]"; if ($expired) { printColor(" (UTLOPT)", 'red'); } echo "\n"; echo " E-post: {$sig['email']}\n"; echo " Navn: {$name}\n"; echo " Kampanje: {$sig['petition_id']}\n"; echo " Dato: {$date}\n"; if (!$expired) { echo " Utloper: om {$daysLeft} dager\n"; } $response = strtolower(prompt("\nIgnorer denne? [j/n/s]: ")); if ($response === 's' || $response === 'stopp' || $response === 'stop') { echo "Avslutter gjennomgang.\n"; break; } if ($response === 'j' || $response === 'ja' || $response === 'y' || $response === 'yes') { $reason = prompt("Grunn (valgfritt): "); if (addToIgnoreList('unconfirmed', $sig['petition_id'], $sig['email'], $reason)) { printColor("Markert som ignorert.\n", 'green'); $ignored++; } else { printColor("Kunne ikke lagre til ignorliste.\n", 'red'); } } else { echo "Beholdt.\n"; $kept++; } } echo "\n"; printColor("Ferdig: {$ignored} ignorert, {$kept} beholdt\n", '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': sendToSpecificEmails(); break; case '6': markAsIgnored(); break; case '7': manuallyConfirmSignatures(); break; case '8': manuallyDeleteSignatures(); break; case '9': case 'q': case 'quit': case 'exit': echo "Ha det!\n"; exit(0); default: printColor("Ugyldig valg. Velg 1-9.\n", 'red'); } } } // Run main();