217 lines
9.6 KiB
PHP
Executable File
217 lines
9.6 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* GAN Adversarial Filter API
|
|
* Simulates Gmail/Outlook/GMX spam filters locally
|
|
* Tests templates BEFORE sending to avoid IP burns
|
|
*/
|
|
header('Content-Type: application/json');
|
|
header('Access-Control-Allow-Origin: *');
|
|
|
|
try {
|
|
$pdo = new PDO('pgsql:host=localhost;dbname=adx_system', 'admin', 'admin123');
|
|
$pdo->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('</body>', '<p style="font-size:11px;color:#999">Pour vous desinscrire: <a href="{unsubscribe_url}">cliquez ici</a></p></body>', $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']]);
|
|
}
|