Name lookup table. */ function getUsableRemailerKeyNames(): array { static $usableNames = null; if (is_array($usableNames)) { return $usableNames; } $usableNames = []; $keyring = yamnConfig('YAMN_PUBRING', '/opt/yamn-master/pubring.mix'); if (!is_readable($keyring)) { return $usableNames; } $today = gmdate('Y-m-d'); foreach (file($keyring, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { $parts = preg_split('/\s+/', trim($line)); if (count($parts) !== 7) { continue; } [$name, , , , , $validFrom, $validUntil] = $parts; if (!preg_match('/^[a-z0-9_-]+$/', $name) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $validFrom) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $validUntil)) { continue; } if ($validFrom <= $today && $today <= $validUntil) { $usableNames[$name] = true; } } return $usableNames; } /** * Parse remailers from file and return array by type * Entry and Exit can use ANY remailer * Middle should use remailers with specific flags * * @param string $type 'entry', 'middle', or 'exit' * @return array Array of remailer names */ function getRemailers($type) { $remailers = ['*']; // Always include Random option $usableKeyNames = getUsableRemailerKeyNames(); // Try multiple file locations $files = [ '/opt/yamn-data/cache/remailers.txt', '/var/www/yamnweb/remailers.txt' ]; foreach ($files as $file) { if (!file_exists($file)) continue; $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $inDataSection = false; foreach ($lines as $line) { $line = trim($line); // Start parsing after separator line (--------) if (preg_match('/^-{10,}/', $line)) { $inDataSection = true; continue; } // Stop at Remailer-Capabilities section if (stripos($line, 'Remailer-Capabilities') !== false) { break; } // Only parse lines after the separator if (!$inDataSection) continue; // Skip empty lines if (empty($line)) continue; // Split line: name latency uptime [flags] // Example: "middleman 211112111211 :45 ++++++++++++ 100.0% D" $parts = preg_split('/\s+/', $line); // Must have at least name + latency if (count($parts) < 2) continue; // Extract ONLY the remailer name (first field) $remailerName = $parts[0]; // Validate name: lowercase letters, numbers, hyphens only if (!preg_match('/^[a-z0-9-]+$/', $remailerName)) continue; if (!isset($usableKeyNames[$remailerName])) continue; // Check if last field is 'D' (middle capability flag) $lastField = end($parts); $isMiddleCapable = ($lastField === 'D'); // LOGICA CORRETTA: Mutuamente esclusivi if ($isMiddleCapable) { // Remailers CON 'D' = SOLO middle if ($type === 'middle') { $remailers[] = $remailerName; } } else { // Remailers SENZA 'D' = SOLO entry/exit if ($type === 'entry' || $type === 'exit') { $remailers[] = $remailerName; } } } break; // Use first file found } return array_unique($remailers); } /** * Replace asterisk (*) with random remailer from available list * @param string $remailer Selected remailer (may be "*") * @param array $availableRemailers List of available remailers * @return string Actual remailer name */ function resolveRemailer($remailer, $availableRemailers) { if ($remailer === '*') { // Filter out the asterisk itself from candidates $candidates = array_filter($availableRemailers, function($r) { return $r !== '*'; }); if (empty($candidates)) { throw new Exception("No remailers available for random selection"); } // Pick random remailer $randomIndex = array_rand($candidates); return $candidates[$randomIndex]; } if (!in_array($remailer, $availableRemailers, true)) { throw new Exception("Selected remailer is no longer available. Reload the page and choose again."); } return $remailer; } // Load available remailers $entryRemailers = getRemailers('entry'); $middleRemailers = getRemailers('middle'); $exitRemailers = getRemailers('exit'); // Refresh stale remailer data during a browser reload. The downloader only // contacts Tor when either local file is missing or older than one day. if ($_SERVER['REQUEST_METHOD'] === 'GET' && class_exists('SecureRemailerDownloader')) { try { $downloader = new SecureRemailerDownloader(); if (!$downloader->downloadRemailers()) { error_log('Remailer refresh failed; keeping the locally available data.'); } // Reload after a successful refresh, or continue with the local list. $entryRemailers = getRemailers('entry'); $middleRemailers = getRemailers('middle'); $exitRemailers = getRemailers('exit'); } catch (Exception $e) { error_log("Failed to download remailers: " . $e->getMessage()); } } $message = ''; $messageType = ''; // Check for flash messages from previous redirect (PRG pattern) if (isset($_SESSION['flash_message'])) { $message = $_SESSION['flash_message']; $messageType = isset($_SESSION['flash_type']) ? $_SESSION['flash_type'] : 'info'; // Clear flash messages after displaying unset($_SESSION['flash_message']); unset($_SESSION['flash_type']); } // Handle form submission if ($_SERVER['REQUEST_METHOD'] === 'POST') { // CSRF validation - IMPROVED with better error handling if (!isset($_POST['csrf_token'])) { // Save error in session and redirect $_SESSION['flash_message'] = "Security token missing. Please reload the page and try again."; $_SESSION['flash_type'] = 'error'; header('Location: ' . $_SERVER['PHP_SELF']); exit; } elseif ($_POST['csrf_token'] !== $currentCsrfToken) { // Save error in session and redirect $_SESSION['flash_message'] = "Security token mismatch. Please try submitting again."; $_SESSION['flash_type'] = 'error'; header('Location: ' . $_SERVER['PHP_SELF']); exit; } else { try { // Get and sanitize form data with additional validation $entryRemailer = isset($_POST['entry_remailer']) ? filter_var($_POST['entry_remailer'], FILTER_SANITIZE_STRING) : ''; $middleRemailer = isset($_POST['middle_remailer']) ? filter_var($_POST['middle_remailer'], FILTER_SANITIZE_STRING) : ''; $exitRemailer = isset($_POST['exit_remailer']) ? filter_var($_POST['exit_remailer'], FILTER_SANITIZE_STRING) : ''; $from = isset($_POST['from']) ? filter_var($_POST['from'], FILTER_SANITIZE_STRING) : ''; $replyTo = isset($_POST['reply_to']) && is_string($_POST['reply_to']) ? trim($_POST['reply_to']) : ''; $to = isset($_POST['to']) ? filter_var($_POST['to'], FILTER_SANITIZE_EMAIL) : ''; $subject = isset($_POST['subject']) ? filter_var($_POST['subject'], FILTER_SANITIZE_STRING) : ''; $newsgroups = isset($_POST['newsgroups']) ? filter_var($_POST['newsgroups'], FILTER_SANITIZE_STRING) : ''; $references = isset($_POST['references']) && is_string($_POST['references']) ? trim($_POST['references']) : ''; $data = isset($_POST['data']) ? $_POST['data'] : ''; // Keep original formatting $copies = isset($_POST['copies']) ? intval($_POST['copies']) : 1; // Validate copies if ($copies < 1 || $copies > 3) { throw new Exception("Number of copies must be between 1 and 3."); } if (empty($from)) { throw new Exception("Sender (From) is required."); } $messageKind = trim($newsgroups) === '' ? 'email' : 'usenet'; if ($messageKind === 'email' && empty($to)) { throw new Exception("Email recipient is required when Newsgroup is empty."); } if ($messageKind === 'usenet') { $to = yamnConfig('YAMN_USENET_GATEWAY', 'mail2news@mail2news.tcpreset.net'); if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { throw new Exception("Usenet gateway is not configured correctly."); } } // Validate remailer chain if (empty($entryRemailer) || empty($middleRemailer) || empty($exitRemailer)) { throw new Exception("All three remailers must be specified."); } // Resolve asterisks (*) to actual random remailers $resolvedEntry = resolveRemailer($entryRemailer, $entryRemailers); $resolvedMiddle = resolveRemailer($middleRemailer, $middleRemailers); $resolvedExit = resolveRemailer($exitRemailer, $exitRemailers); $encoded = encodeYamnMessage([ 'kind' => $messageKind, 'entry' => $resolvedEntry, 'chain' => [$resolvedEntry, $resolvedMiddle, $resolvedExit], 'from' => $from, 'reply_to' => $replyTo, 'to' => $to, 'subject' => $subject, 'newsgroup' => $newsgroups, 'body' => $data, 'references' => $references, ]); if (!$encoded['success']) { throw new Exception($encoded['error'] ?? 'YAMN encoding failed'); } $responseFinished = false; $onHandoff = null; if (function_exists('fastcgi_finish_request')) { $onHandoff = static function () use (&$responseFinished): void { ignore_user_abort(true); $_SESSION['flash_message'] = 'Message accepted into the local in-memory Nym queue. Delivery continues in the background; YAMN queues may take several hours, and this confirmation does not mean the message has been published or delivered yet.'; $_SESSION['flash_type'] = 'success'; $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); session_write_close(); header('Location: ' . $_SERVER['PHP_SELF']); fastcgi_finish_request(); $responseFinished = true; }; } $result = sendNymEnvelope($encoded['envelope'], 1, $encoded['entry_address'], $onHandoff); if ($responseFinished) { exit; } if (!$result['success']) { throw new Exception($result['error'] ?? 'Nym submission failed'); } $_SESSION['flash_message'] = 'Message accepted into the local in-memory Nym queue. Delivery through the YAMN queues may take several hours; this confirmation does not mean the message has been published or delivered yet.'; $_SESSION['flash_type'] = 'success'; $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); header('Location: ' . $_SERVER['PHP_SELF']); exit; } catch (Exception $e) { // Save error in session and redirect (PRG pattern for errors too) $_SESSION['flash_message'] = "✗ Error: " . $e->getMessage(); $_SESSION['flash_type'] = 'error'; error_log("YAMN submission error: " . $e->getMessage() . "\nStack: " . $e->getTraceAsString()); // Redirect to prevent form resubmission header('Location: ' . $_SERVER['PHP_SELF']); exit; } catch (Error $e) { // Catch PHP 7+ Error class (for fatal errors) $_SESSION['flash_message'] = "✗ Fatal Error: " . $e->getMessage(); $_SESSION['flash_type'] = 'error'; error_log("YAMN fatal error: " . $e->getMessage() . "\nStack: " . $e->getTraceAsString()); // Redirect to prevent form resubmission header('Location: ' . $_SERVER['PHP_SELF']); exit; } catch (Throwable $e) { // Catch everything else $_SESSION['flash_message'] = "✗ Unexpected error: " . $e->getMessage(); $_SESSION['flash_type'] = 'error'; error_log("YAMN unexpected error: " . $e->getMessage()); // Redirect to prevent form resubmission header('Location: ' . $_SERVER['PHP_SELF']); exit; } } } ?>
Remailer statistics and public keys are checked automatically when this page loads. Local data no more than 24 hours old is reused without a network connection.
Missing or older data is downloaded through Tor. If a source is unavailable, the existing data remains usable and another download is not attempted until the next daily check.