Add ignore list functionality for petition management

Add functionality to mark entries as ignored for malformed emails and
other issues
Add ignore list file and management functions
Update menu to include ignore option
Implement interactive ignore marking for failed emails and unconfirmed
signatures
Add color-coded output for better visibility
Update function signatures to support ignore list filtering
This commit is contained in:
Ruben 2026-01-24 22:44:08 +01:00
parent 7659b0cf5e
commit 5194ba8213

View file

@ -7,6 +7,7 @@
* - View failed email sends from SMTP log * - View failed email sends from SMTP log
* - View unconfirmed signatures from petition CSVs * - View unconfirmed signatures from petition CSVs
* - Retry sending emails with rate limiting (250/hour max) * - Retry sending emails with rate limiting (250/hour max)
* - Mark entries as ignored (for malformed emails etc.)
* *
* Usage: php custom/petition-cli.php * Usage: php custom/petition-cli.php
*/ */
@ -20,6 +21,7 @@ define('BASE_DIR', __DIR__);
define('DATA_DIR', BASE_DIR . '/data'); define('DATA_DIR', BASE_DIR . '/data');
define('PETITIONS_DIR', DATA_DIR . '/petitions'); define('PETITIONS_DIR', DATA_DIR . '/petitions');
define('SMTP_LOG', DATA_DIR . '/smtp-log.csv'); 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 // Rate limit: 250 emails/hour = 1 email per 14.4 seconds, using 15 seconds
define('EMAIL_DELAY_SECONDS', 15); define('EMAIL_DELAY_SECONDS', 15);
@ -49,14 +51,93 @@ function getPetitionFiles(): array {
}, $files); }, $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 * Read failed emails from SMTP log
*/ */
function getFailedEmails(): array { function getFailedEmails(bool $excludeIgnored = true): array {
if (!file_exists(SMTP_LOG)) { if (!file_exists(SMTP_LOG)) {
return []; return [];
} }
$ignoreList = $excludeIgnored ? loadIgnoreList() : [];
$failed = []; $failed = [];
$fp = fopen(SMTP_LOG, 'r'); $fp = fopen(SMTP_LOG, 'r');
if (!$fp) { if (!$fp) {
@ -69,13 +150,20 @@ function getFailedEmails(): array {
// CSV format: timestamp, type, petition_id, email, status, error_message // CSV format: timestamp, type, petition_id, email, status, error_message
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[4]) && $row[4] === 'failed') { if (isset($row[4]) && $row[4] === 'failed') {
$failed[] = [ $entry = [
'timestamp' => (int)$row[0], 'timestamp' => (int)$row[0],
'type' => $row[1], 'type' => $row[1],
'petition_id' => $row[2], 'petition_id' => $row[2],
'email' => $row[3], 'email' => $row[3],
'error' => $row[5] ?? '' '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 * 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'; $csvPath = PETITIONS_DIR . '/' . $petitionId . '.csv';
if (!file_exists($csvPath)) { if (!file_exists($csvPath)) {
return []; return [];
} }
$ignoreList = $excludeIgnored ? loadIgnoreList() : [];
$unconfirmed = []; $unconfirmed = [];
$fp = fopen($csvPath, 'r'); $fp = fopen($csvPath, 'r');
if (!$fp) { if (!$fp) {
@ -120,7 +210,7 @@ function getUnconfirmedSignatures(string $petitionId): array {
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) { while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (isset($row[6]) && $row[6] === 'pending') { if (isset($row[6]) && $row[6] === 'pending') {
$tokenCreated = isset($row[8]) ? (int)$row[8] : (int)$row[0]; $tokenCreated = isset($row[8]) ? (int)$row[8] : (int)$row[0];
$unconfirmed[] = [ $entry = [
'timestamp' => (int)$row[0], 'timestamp' => (int)$row[0],
'email' => $row[1], 'email' => $row[1],
'firstname' => $row[2], 'firstname' => $row[2],
@ -131,6 +221,13 @@ function getUnconfirmedSignatures(string $petitionId): array {
'token_created' => $tokenCreated, 'token_created' => $tokenCreated,
'petition_id' => $petitionId '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 * Get all unconfirmed signatures from all petitions
*/ */
function getAllUnconfirmedSignatures(): array { function getAllUnconfirmedSignatures(bool $excludeIgnored = true): array {
$all = []; $all = [];
foreach (getPetitionFiles() as $petitionId) { foreach (getPetitionFiles() as $petitionId) {
$all = array_merge($all, getUnconfirmedSignatures($petitionId)); $all = array_merge($all, getUnconfirmedSignatures($petitionId, $excludeIgnored));
} }
// Sort by timestamp descending // Sort by timestamp descending
@ -268,6 +365,7 @@ function printColor(string $text, string $color): void {
'green' => "\033[32m", 'green' => "\033[32m",
'yellow' => "\033[33m", 'yellow' => "\033[33m",
'blue' => "\033[34m", 'blue' => "\033[34m",
'cyan' => "\033[36m",
'reset' => "\033[0m" 'reset' => "\033[0m"
]; ];
@ -302,7 +400,8 @@ function showMenu(): void {
echo " 2) Vis ubekreftede signaturer\n"; echo " 2) Vis ubekreftede signaturer\n";
echo " 3) Send e-post pa nytt til mislykkede\n"; echo " 3) Send e-post pa nytt til mislykkede\n";
echo " 4) Send bekreftelse pa nytt til ubekreftede\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"; echo "\n";
} }
@ -537,6 +636,153 @@ function resendToUnconfirmed(): void {
printColor("Ferdig: {$success} vellykket, {$failures} mislykket\n", $failures > 0 ? 'yellow' : 'green'); 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 // Main loop
function main(): void { function main(): void {
// Check requirements // Check requirements
@ -568,13 +814,16 @@ function main(): void {
resendToUnconfirmed(); resendToUnconfirmed();
break; break;
case '5': case '5':
markAsIgnored();
break;
case '6':
case 'q': case 'q':
case 'quit': case 'quit':
case 'exit': case 'exit':
echo "Ha det!\n"; echo "Ha det!\n";
exit(0); exit(0);
default: default:
printColor("Ugyldig valg. Velg 1-5.\n", 'red'); printColor("Ugyldig valg. Velg 1-6.\n", 'red');
} }
} }
} }