diff --git a/custom/petition-cli.php b/custom/petition-cli.php index 83746b2..7a746f8 100755 --- a/custom/petition-cli.php +++ b/custom/petition-cli.php @@ -7,6 +7,7 @@ * - View failed email sends from SMTP log * - View unconfirmed signatures from petition CSVs * - Retry sending emails with rate limiting (250/hour max) + * - Mark entries as ignored (for malformed emails etc.) * * Usage: php custom/petition-cli.php */ @@ -20,6 +21,7 @@ define('BASE_DIR', __DIR__); define('DATA_DIR', BASE_DIR . '/data'); define('PETITIONS_DIR', DATA_DIR . '/petitions'); define('SMTP_LOG', DATA_DIR . '/smtp-log.csv'); +define('IGNORE_LIST', DATA_DIR . '/petition-ignore.csv'); // Rate limit: 250 emails/hour = 1 email per 14.4 seconds, using 15 seconds define('EMAIL_DELAY_SECONDS', 15); @@ -49,14 +51,93 @@ function getPetitionFiles(): array { }, $files); } +/** + * Load ignore list + */ +function loadIgnoreList(): array { + if (!file_exists(IGNORE_LIST)) { + return []; + } + + $ignored = []; + $fp = fopen(IGNORE_LIST, 'r'); + if (!$fp) { + return []; + } + + // Skip header + fgetcsv($fp, null, ',', '"', ''); + + // CSV format: timestamp, type, petition_id, email, reason + while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { + if (isset($row[3])) { + // Key format: type|petition_id|email + $key = ($row[1] ?? '') . '|' . ($row[2] ?? '') . '|' . strtolower($row[3]); + $ignored[$key] = [ + 'timestamp' => (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(): array { +function getFailedEmails(bool $excludeIgnored = true): array { if (!file_exists(SMTP_LOG)) { return []; } + $ignoreList = $excludeIgnored ? loadIgnoreList() : []; + $failed = []; $fp = fopen(SMTP_LOG, 'r'); if (!$fp) { @@ -69,13 +150,20 @@ function getFailedEmails(): array { // CSV format: timestamp, type, petition_id, email, status, error_message while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { if (isset($row[4]) && $row[4] === 'failed') { - $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; } } @@ -101,12 +189,14 @@ function getFailedEmails(): array { /** * Get unconfirmed signatures from a petition CSV */ -function getUnconfirmedSignatures(string $petitionId): array { +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) { @@ -120,7 +210,7 @@ function getUnconfirmedSignatures(string $petitionId): array { while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { if (isset($row[6]) && $row[6] === 'pending') { $tokenCreated = isset($row[8]) ? (int)$row[8] : (int)$row[0]; - $unconfirmed[] = [ + $entry = [ 'timestamp' => (int)$row[0], 'email' => $row[1], 'firstname' => $row[2], @@ -131,6 +221,13 @@ function getUnconfirmedSignatures(string $petitionId): array { 'token_created' => $tokenCreated, 'petition_id' => $petitionId ]; + + // Skip if ignored + if ($excludeIgnored && isIgnored($ignoreList, 'unconfirmed', $petitionId, $entry['email'])) { + continue; + } + + $unconfirmed[] = $entry; } } @@ -145,10 +242,10 @@ function getUnconfirmedSignatures(string $petitionId): array { /** * Get all unconfirmed signatures from all petitions */ -function getAllUnconfirmedSignatures(): array { +function getAllUnconfirmedSignatures(bool $excludeIgnored = true): array { $all = []; foreach (getPetitionFiles() as $petitionId) { - $all = array_merge($all, getUnconfirmedSignatures($petitionId)); + $all = array_merge($all, getUnconfirmedSignatures($petitionId, $excludeIgnored)); } // Sort by timestamp descending @@ -268,6 +365,7 @@ function printColor(string $text, string $color): void { 'green' => "\033[32m", 'yellow' => "\033[33m", 'blue' => "\033[34m", + 'cyan' => "\033[36m", 'reset' => "\033[0m" ]; @@ -302,7 +400,8 @@ function showMenu(): void { 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 " 5) Marker oppforinger som ignorert\n"; + echo " 6) Avslutt\n"; echo "\n"; } @@ -537,6 +636,153 @@ function resendToUnconfirmed(): void { printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : '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 @@ -568,13 +814,16 @@ function main(): void { resendToUnconfirmed(); break; case '5': + markAsIgnored(); + break; + case '6': case 'q': case 'quit': case 'exit': echo "Ha det!\n"; exit(0); default: - printColor("Ugyldig valg. Velg 1-5.\n", 'red'); + printColor("Ugyldig valg. Velg 1-6.\n", 'red'); } } }