innhold/custom/petition-cli.php
Ruben fffd51422c Add signature deletion functionality to petition CLI
Introduce new modifyPetitionFile helper function for atomic CSV
operations
Refactor manual confirmation to use the new helper function
Add new manuallyDeleteSignatures function with confirmation flow
Update menu to include deletion option and adjust numbering
2026-02-01 20:49:06 +01:00

1153 lines
34 KiB
PHP
Executable file

#!/usr/bin/env php
<?php
/**
* Petition Email CLI Tool
*
* Interactive tool for managing petition emails:
* - 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
*/
// Ensure CLI only
if (php_sapi_name() !== 'cli') {
die("This script can only be run from the command line.\n");
}
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);
/**
* Load SMTP configuration
*/
function loadSmtpConfig(): ?array {
$configPath = BASE_DIR . '/smtp-config.php';
if (!file_exists($configPath)) {
return null;
}
return require $configPath;
}
/**
* Get all petition CSV files
*/
function getPetitionFiles(): array {
if (!is_dir(PETITIONS_DIR)) {
return [];
}
$files = glob(PETITIONS_DIR . '/*.csv');
return array_map(function($f) {
return basename($f, '.csv');
}, $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(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, '&#10007;') !== 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",
'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) Marker oppforinger som ignorert\n";
echo " 6) Manuell bekreftelse av signaturer\n";
echo " 7) Slett signaturer\n";
echo " 8) 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');
}
/**
* 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':
markAsIgnored();
break;
case '6':
manuallyConfirmSignatures();
break;
case '7':
manuallyDeleteSignatures();
break;
case '8':
case 'q':
case 'quit':
case 'exit':
echo "Ha det!\n";
exit(0);
default:
printColor("Ugyldig valg. Velg 1-8.\n", 'red');
}
}
}
// Run
main();