exec('SET search_path TO admin, public'); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo json_encode(['error' => 'DB: ' . $e->getMessage()]); exit; } $input = json_decode(file_get_contents('php://input'), true) ?: $_REQUEST; $action = $input['action'] ?? $_GET['action'] ?? 'status'; // ISP Filter simulation weights function simulateFilter($pdo, $isp, $subject, $body, $headers = []) { $score = 100; // Start at 100 (perfect) $flags = []; // Load rules for this ISP $stmt = $pdo->prepare("SELECT * FROM gan_filter_rules WHERE (isp = ? OR isp = 'ALL') AND status = 'active' ORDER BY weight DESC"); $stmt->execute([$isp]); $rules = $stmt->fetchAll(PDO::FETCH_ASSOC); $fullContent = strtolower($subject . ' ' . $body); foreach ($rules as $rule) { $hit = false; switch ($rule['rule_type']) { case 'subject': $keywords = explode(',', $rule['pattern']); foreach ($keywords as $kw) { if (stripos($subject, trim($kw)) !== false) { $hit = true; break; } } break; case 'body': if (preg_match('/href/i', $body)) { $linkCount = substr_count(strtolower($body), 'href'); if ($linkCount > 5) $hit = true; } if (stripos($rule['pattern'], 'image ratio') !== false) { $textLen = strlen(strip_tags($body)); if ($textLen < 100) $hit = true; } break; case 'headers': if (stripos($rule['pattern'], 'List-Unsubscribe') !== false) { if (empty($headers['List-Unsubscribe'])) $hit = true; } if (stripos($rule['pattern'], 'X-Mailer') !== false) { if (!empty($headers['X-Mailer'])) $hit = true; } break; case 'auth': // DKIM/SPF checks if (stripos($rule['pattern'], 'DKIM=none') !== false) { if (empty($headers['DKIM-Signature'])) $hit = true; } break; case 'network': // Datacenter detection if (stripos($rule['pattern'], 'Hetzner') !== false) { $hit = true; // We ARE on Hetzner } break; } if ($hit) { $penalty = (float)$rule['weight'] * 15; $score -= $penalty; $flags[] = [ 'rule' => $rule['rule_name'], 'type' => $rule['rule_type'], 'penalty' => round($penalty, 1), 'weight' => $rule['weight'] ]; } } // Content analysis $spamWords = ['gratuit','urgent','offre','promo','gagnez','felicitations','100%','garanti','sans engagement','cliquez ici']; $spamCount = 0; foreach ($spamWords as $w) { if (stripos($fullContent, $w) !== false) $spamCount++; } if ($spamCount > 0) { $penalty = $spamCount * 5; $score -= $penalty; $flags[] = ['rule' => "Spam words detected: $spamCount", 'type' => 'content', 'penalty' => $penalty]; } // HTML ratio $htmlLen = strlen($body); $textLen = strlen(strip_tags($body)); if ($htmlLen > 0 && ($textLen / $htmlLen) < 0.3) { $score -= 10; $flags[] = ['rule' => 'Low text/HTML ratio', 'type' => 'structure', 'penalty' => 10]; } // URL analysis preg_match_all('/https?:\/\/[^\s"\'<]+/', $body, $urls); $uniqueDomains = []; foreach ($urls[0] ?? [] as $url) { $host = parse_url($url, PHP_URL_HOST); if ($host) $uniqueDomains[$host] = true; } if (count($uniqueDomains) > 3) { $score -= 8; $flags[] = ['rule' => 'Multiple domains in links: ' . count($uniqueDomains), 'type' => 'urls', 'penalty' => 8]; } $score = max(0, min(100, $score)); $verdict = $score >= 80 ? 'INBOX' : ($score >= 50 ? 'PROMO/JUNK' : 'SPAM'); return [ 'score' => round($score, 1), 'verdict' => $verdict, 'flags' => $flags, 'suggestions' => generateSuggestions($flags), 'rules_checked' => count($rules) ]; } function generateSuggestions($flags) { $suggestions = []; foreach ($flags as $f) { switch ($f['type'] ?? '') { case 'subject': $suggestions[] = 'Rewrite subject without spam trigger words'; break; case 'headers': $suggestions[] = 'Add List-Unsubscribe header, remove X-Mailer'; break; case 'auth': $suggestions[] = 'Ensure DKIM signing and SPF pass'; break; case 'network': $suggestions[] = 'Route via residential proxy or O365'; break; case 'content': $suggestions[] = 'Replace spam words with neutral alternatives'; break; case 'structure': $suggestions[] = 'Increase text content, reduce HTML boilerplate'; break; case 'urls': $suggestions[] = 'Use single tracking domain for all links'; break; } } return array_unique($suggestions); } function mutateTemplate($subject, $body) { // Auto-improve template to pass filters $replacements = [ 'gratuit' => 'offert', 'urgent' => 'important', 'promo' => 'selection', 'gagnez' => 'decouvrez', 'cliquez ici' => 'en savoir plus', 'felicitations' => 'bonjour', '100%' => 'totalement', 'GRATUIT' => 'OFFERT', 'URGENT' => 'PRIORITAIRE' ]; $newSubject = str_ireplace(array_keys($replacements), array_values($replacements), $subject); $newBody = str_ireplace(array_keys($replacements), array_values($replacements), $body); // Add unsubscribe link if missing if (stripos($newBody, 'unsubscribe') === false && stripos($newBody, 'desinscrire') === false) { $newBody = str_ireplace('', '

Pour vous desinscrire: cliquez ici

', $newBody); } return ['subject' => $newSubject, 'body' => $newBody, 'mutations' => count($replacements)]; } switch ($action) { case 'test': $subject = $input['subject'] ?? ''; $body = $input['body'] ?? $input['body_html'] ?? ''; $isp = $input['isp'] ?? 'Gmail'; $headers = $input['headers'] ?? []; if (empty($subject) && empty($body)) { echo json_encode(['error' => 'subject and body required']); exit; } $result = simulateFilter($pdo, $isp, $subject, $body, $headers); // Log $pdo->prepare("INSERT INTO gan_test_results (template_name, isp_target, score_before, passed, details, tested_at) VALUES (?,?,?,?,?,NOW())") ->execute([substr($subject, 0, 200), $isp, $result['score'], $result['verdict'] === 'INBOX', json_encode($result)]); echo json_encode($result); break; case 'test_all': $subject = $input['subject'] ?? ''; $body = $input['body'] ?? ''; $results = []; foreach (['Gmail','Outlook','GMX','T-Online','Yahoo'] as $isp) { $results[$isp] = simulateFilter($pdo, $isp, $subject, $body, $input['headers'] ?? []); } echo json_encode(['results' => $results, 'timestamp' => date('c')]); break; case 'mutate': $subject = $input['subject'] ?? ''; $body = $input['body'] ?? ''; $isp = $input['isp'] ?? 'Gmail'; $before = simulateFilter($pdo, $isp, $subject, $body); $mutated = mutateTemplate($subject, $body); $after = simulateFilter($pdo, $isp, $mutated['subject'], $mutated['body']); $pdo->prepare("INSERT INTO gan_test_results (template_name, isp_target, score_before, score_after, mutations_applied, passed, details, tested_at) VALUES (?,?,?,?,?,?,?,NOW())") ->execute([substr($subject, 0, 200), $isp, $before['score'], $after['score'], $mutated['mutations'], $after['verdict'] === 'INBOX', json_encode(['before' => $before, 'after' => $after])]); echo json_encode(['before' => $before, 'after' => $after, 'mutated' => $mutated, 'improvement' => round($after['score'] - $before['score'], 1)]); break; case 'rules': $isp = $input['isp'] ?? $_GET['isp'] ?? null; $q = "SELECT * FROM gan_filter_rules WHERE status = 'active'"; if ($isp) { $q .= " AND isp = '$isp'"; } $q .= " ORDER BY isp, weight DESC"; echo json_encode(['rules' => $pdo->query($q)->fetchAll(PDO::FETCH_ASSOC)]); break; case 'stats': $stats = $pdo->query("SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE passed) as passed, COUNT(*) FILTER (WHERE NOT passed) as failed, ROUND(AVG(score_before),1) as avg_score, COUNT(*) FILTER (WHERE tested_at >= CURRENT_DATE) as today FROM gan_test_results")->fetch(PDO::FETCH_ASSOC); $byIsp = $pdo->query("SELECT isp_target, COUNT(*) as tests, ROUND(AVG(score_before),1) as avg_score, COUNT(*) FILTER (WHERE passed) as passed FROM gan_test_results GROUP BY isp_target")->fetchAll(PDO::FETCH_ASSOC); $rules = $pdo->query("SELECT isp, COUNT(*) as count FROM gan_filter_rules WHERE status='active' GROUP BY isp")->fetchAll(PDO::FETCH_ASSOC); echo json_encode(['stats' => $stats, 'by_isp' => $byIsp, 'rules' => $rules]); break; default: echo json_encode(['status' => 'online', 'service' => 'GAN Adversarial Filter', 'version' => '1.0', 'actions' => ['test','test_all','mutate','rules','stats']]); }