Files
wevads-platform/scripts/mta_helper.php
2026-02-26 04:53:11 +01:00

426 lines
20 KiB
PHP
Executable File

<?php
// MTA Helper V4 — Brain-Driven Smart Routing
// Reads ALL methods from brain_configs (134 configs, 45 methods, 22 types)
// Brain picks winner per ISP, falls back to exploration
require_once("/opt/wevads/config/pipeline_hooks.php");
function smart_route_send($to, $subject, $html, $domain='wevup.app', $from_name='Info', $from_email='') {
$isp = strtoupper(detect_isp($to));
$db = @pg_connect("host=localhost dbname=adx_system user=admin password=admin123");
if(!$db) return mta_eu_send($to, $subject, $html, $domain, $from_name, $from_email);
@pg_query($db, "SET search_path TO admin,public");
// 1. Check brain_configs for a winner config for this ISP
$r = @pg_query_params($db, "SELECT id, send_method, from_name as cfg_from_name, from_email as cfg_from_email,
domain_used, smtp_type, smtp_account_id, inbox_rate,
header_1_name, header_1_value, header_2_name, header_2_value,
header_3_name, header_3_value, header_4_name, header_4_value,
content_type, encoding, charset, dkim_selector, ip_used, return_path
FROM brain_configs
WHERE isp_target=$1 AND is_winner=true AND is_active=true
ORDER BY inbox_rate DESC, consecutive_inbox DESC LIMIT 1", [$isp]);
$winner = $r ? @pg_fetch_assoc($r) : null;
// 2. If no winner, check isp_methods for default method
if(!$winner) {
$r2 = @pg_query_params($db, "SELECT method FROM isp_methods WHERE isp_name=$1 LIMIT 1", [$isp]);
$isp_method = $r2 ? @pg_fetch_assoc($r2) : null;
if($isp_method) {
// Get best config for this ISP+method combo
$r3 = @pg_query_params($db, "SELECT id, send_method, from_name as cfg_from_name, from_email as cfg_from_email,
domain_used, smtp_type, smtp_account_id, inbox_rate,
header_1_name, header_1_value, header_2_name, header_2_value,
header_3_name, header_3_value, header_4_name, header_4_value,
content_type, encoding, charset, dkim_selector, ip_used, return_path
FROM brain_configs
WHERE isp_target=$1 AND send_method=$2 AND is_active=true
ORDER BY inbox_rate DESC LIMIT 1", [$isp, $isp_method['method']]);
$winner = $r3 ? @pg_fetch_assoc($r3) : null;
}
}
// 3. If still no config, explore least-tested method
if(!$winner) {
$r4 = @pg_query_params($db, "SELECT id, send_method, from_name as cfg_from_name, from_email as cfg_from_email,
domain_used, smtp_type, smtp_account_id, inbox_rate,
header_1_name, header_1_value, header_2_name, header_2_value,
header_3_name, header_3_value, header_4_name, header_4_value,
content_type, encoding, charset, dkim_selector, ip_used, return_path
FROM brain_configs
WHERE isp_target=$1 AND is_active=true
ORDER BY total_sent ASC, inbox_rate DESC LIMIT 1", [$isp]);
$winner = $r4 ? @pg_fetch_assoc($r4) : null;
}
// Execute the chosen method
$method = $winner ? $winner['send_method'] : 'MTA_EU_RELAY';
$config_id = $winner ? $winner['id'] : 0;
$result = execute_send_method($to, $subject, $html, $domain, $from_name, $from_email, $method, $winner, $db);
// Log for brain learning
$status = strpos($result, 'OK') !== false ? 'sent' : 'failed';
@pg_query_params($db, "UPDATE brain_configs SET total_sent=COALESCE(total_sent,0)+1, updated_at=NOW() WHERE id=$1", [$config_id]);
brain_log_send(strtolower($isp), $method, $to, $status);
return $result;
}
function execute_send_method($to, $subject, $html, $domain, $from_name, $from_email, $method, $config, $db) {
// Template vars
$html = process_template_vars($html, $to);
$subject = process_template_vars($subject, $to);
switch($method) {
case 'O365_GRAPH':
case 'GRAPH_API':
return o365_graph_send($to, $subject, $html);
case 'OFFICE_365':
case 'O365_SMTP':
case 'O365_EXCHANGE':
case 'PMTA_O365':
return o365_smtp_send($to, $subject, $html, $config, $db);
case 'GSUITE_RELAY':
case 'GSUITE_SMTP':
return gsuite_relay_send($to, $subject, $html, $config);
case 'DOMAIN_IP':
case 'DIRECT_MX':
case 'PMTA_DIRECT':
return mta_eu_send($to, $subject, $html, $domain, $from_name, $from_email);
case 'MTA_EU_RELAY':
return mta_eu_send($to, $subject, $html, $domain, $from_name, $from_email);
case 'SENDGRID':
case 'MAILGUN':
case 'AMAZON_SES':
case 'BREVO':
case 'MAILJET':
case 'SPARKPOST':
case 'POSTMARK':
case 'ELASTICEMAIL':
case 'TURBOSMTP':
case 'SMTP2GO':
case 'ZOHO':
return smtp_api_send($to, $subject, $html, $method, $config, $db);
case 'FIREBASE':
return "SKIP:firebase_not_email";
default:
// Try MTA-EU as universal fallback
return mta_eu_send($to, $subject, $html, $domain, $from_name, $from_email);
}
}
// O365 SMTP Send (for accounts without Graph API — uses SMTP auth)
function o365_smtp_send($to, $subject, $html, $config, $db) {
// Pick a valid O365 account
$r = @pg_query($db, "SELECT admin_email, admin_password FROM office_accounts
WHERE status='Active' AND (password_status='valid' OR password_status='graph_ok')
AND admin_password IS NOT NULL AND admin_password != ''
ORDER BY RANDOM() LIMIT 1");
$acc = $r ? @pg_fetch_assoc($r) : null;
if(!$acc || !$acc['admin_password']) return "FAIL:no_o365_smtp_account";
$from = $acc['admin_email'];
$pass = $acc['admin_password'];
// Check if password is encrypted
if(function_exists('wevads_decrypt')) $pass = wevads_decrypt($pass);
// Build headers from config
$headers = build_headers_from_config($from, $to, $subject, $config);
// Connect via SMTP to O365
$fp = @fsockopen('ssl://smtp.office365.com', 587, $errno, $errstr, 10);
if(!$fp) {
// Try STARTTLS on 587
$fp = @fsockopen('tcp://smtp.office365.com', 587, $errno, $errstr, 10);
if(!$fp) return "FAIL:O365_SMTP:$errstr";
$resp = fgets($fp, 512);
fputs($fp, "EHLO wevup.app\r\n");
while($l = fgets($fp, 512)) { if(substr($l,3,1)==' ') break; }
fputs($fp, "STARTTLS\r\n"); $resp = fgets($fp, 512);
stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
}
fputs($fp, "EHLO wevup.app\r\n");
while($l = fgets($fp, 512)) { if(substr($l,3,1)==' ') break; }
fputs($fp, "AUTH LOGIN\r\n"); $resp = fgets($fp, 512);
fputs($fp, base64_encode($from)."\r\n"); $resp = fgets($fp, 512);
fputs($fp, base64_encode($pass)."\r\n"); $resp = fgets($fp, 512);
if(substr($resp,0,3)!='235') { fclose($fp); return "FAIL:O365_AUTH:$resp"; }
fputs($fp, "MAIL FROM:<$from>\r\n"); $resp = fgets($fp, 512);
fputs($fp, "RCPT TO:<$to>\r\n"); $resp = fgets($fp, 512);
fputs($fp, "DATA\r\n"); $resp = fgets($fp, 512);
fputs($fp, $headers . "\r\n" . $html . "\r\n.\r\n"); $resp = fgets($fp, 512);
fputs($fp, "QUIT\r\n"); fclose($fp);
return substr($resp,0,3)=='250' ? "OK:O365_SMTP:$from" : "FAIL:O365_SMTP:$resp";
}
// GSuite Relay Send
function gsuite_relay_send($to, $subject, $html, $config) {
// Uses smtp-relay.gmail.com with authorized IP
$fp = @fsockopen('smtp-relay.gmail.com', 25, $errno, $errstr, 10);
if(!$fp) return "FAIL:GSUITE:$errstr";
$domain = $config['domain_used'] ?? 'wevup.app';
$from = $config['cfg_from_email'] ?? "info@$domain";
$resp = fgets($fp, 512);
fputs($fp, "EHLO $domain\r\n"); $resp = fgets($fp, 512);
while(substr($resp,3,1)=='-') $resp = fgets($fp, 512);
fputs($fp, "MAIL FROM:<$from>\r\n"); $resp = fgets($fp, 512);
fputs($fp, "RCPT TO:<$to>\r\n"); $resp = fgets($fp, 512);
fputs($fp, "DATA\r\n"); $resp = fgets($fp, 512);
$headers = "From: " . ($config['cfg_from_name'] ?? 'Info') . " <$from>\r\n";
$headers .= "To: $to\r\nSubject: $subject\r\nDate: " . date('r') . "\r\n";
$headers .= "MIME-Version: 1.0\r\nContent-Type: text/html; charset=utf-8\r\n";
fputs($fp, $headers . "\r\n" . $html . "\r\n.\r\n"); $resp = fgets($fp, 512);
fputs($fp, "QUIT\r\n"); fclose($fp);
return substr($resp,0,3)=='250' ? "OK:GSUITE_RELAY" : "FAIL:GSUITE:$resp";
}
// Generic SMTP/API Send for third-party providers
function smtp_api_send($to, $subject, $html, $method, $config, $db) {
// Get SMTP config from send_methods table
$r = @pg_query_params($db, "SELECT config FROM send_methods WHERE method_key=$1", [$method]);
$sm = $r ? @pg_fetch_assoc($r) : null;
if(!$sm) return "FAIL:no_config_for_$method";
$cfg = json_decode($sm['config'], true);
$host = $cfg['host'] ?? '';
$port = $cfg['port'] ?? 587;
if(!$host) return "FAIL:no_host_for_$method";
// Check smtp_configs for credentials
$r2 = @pg_query_params($db, "SELECT username, password, api_key FROM smtp_configs WHERE provider=$1 AND is_active=true LIMIT 1", [strtolower($method)]);
$creds = $r2 ? @pg_fetch_assoc($r2) : null;
if(!$creds) return "FAIL:no_creds_for_$method";
$from = $creds['username'] ?? "info@wevup.app";
// SMTP connect
$fp = @fsockopen("tcp://$host", $port, $errno, $errstr, 10);
if(!$fp) return "FAIL:{$method}_CONNECT:$errstr";
$resp = fgets($fp, 512);
fputs($fp, "EHLO wevup.app\r\n");
while($l = fgets($fp, 512)) { if(substr($l,3,1)==' ') break; }
fputs($fp, "STARTTLS\r\n"); $resp = fgets($fp, 512);
if(substr($resp,0,3)=='220') stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
fputs($fp, "EHLO wevup.app\r\n");
while($l = fgets($fp, 512)) { if(substr($l,3,1)==' ') break; }
// AUTH
$user = $creds['api_key'] ?? $creds['username'] ?? '';
$pass = $creds['password'] ?? '';
fputs($fp, "AUTH LOGIN\r\n"); fgets($fp, 512);
fputs($fp, base64_encode($user)."\r\n"); fgets($fp, 512);
fputs($fp, base64_encode($pass)."\r\n"); $resp = fgets($fp, 512);
if(substr($resp,0,3)!='235') { fclose($fp); return "FAIL:{$method}_AUTH:$resp"; }
fputs($fp, "MAIL FROM:<$from>\r\n"); fgets($fp, 512);
fputs($fp, "RCPT TO:<$to>\r\n"); fgets($fp, 512);
fputs($fp, "DATA\r\n"); fgets($fp, 512);
$headers = "From: Info <$from>\r\nTo: $to\r\nSubject: $subject\r\n";
$headers .= "Date: ".date('r')."\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=utf-8\r\n";
fputs($fp, $headers . "\r\n" . $html . "\r\n.\r\n"); $resp = fgets($fp, 512);
fputs($fp, "QUIT\r\n"); fclose($fp);
return substr($resp,0,3)=='250' ? "OK:$method" : "FAIL:$method:$resp";
}
// Build headers from brain_config
function build_headers_from_config($from, $to, $subject, $config) {
$headers = "From: " . ($config['cfg_from_name'] ?? 'Info') . " <$from>\r\n";
$headers .= "To: $to\r\nSubject: $subject\r\nDate: " . date('r') . "\r\n";
$headers .= "MIME-Version: 1.0\r\n";
// Add custom headers from config
for($i=1; $i<=4; $i++) {
$name = $config["header_{$i}_name"] ?? '';
$value = $config["header_{$i}_value"] ?? '';
if($name && $value) $headers .= "$name: $value\r\n";
}
$ct = $config['content_type'] ?? 'text/html; charset=utf-8';
$headers .= "Content-Type: $ct\r\n";
return $headers;
}
// ============================================================
// EXISTING FUNCTIONS (preserved from V3)
// ============================================================
function mta_eu_send($to, $subject, $html, $domain='wevup.app', $from_name='Info', $from_email='') {
if(!$from_email) $from_email = "info@$domain";
$html = process_template_vars($html, $to);
$subject = process_template_vars($subject, $to);
$payload = json_encode(['to'=>$to,'subject'=>$subject,'body'=>$html,'domain'=>$domain,'from_name'=>$from_name,'from_email'=>$from_email]);
$tmpLocal = tempnam('/tmp', 'mta_');
file_put_contents($tmpLocal, $payload);
$b64 = base64_encode($payload);
$remoteFile = '/tmp/send_' . basename($tmpLocal);
$cmd = "sshpass -p 'HLkFvJJf3uJv' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@89.167.1.139 " .
"'echo " . escapeshellarg($b64) . " | base64 -d > " . $remoteFile .
" && python3 /opt/bcg_local.py " . $remoteFile .
" && rm -f " . $remoteFile . "' 2>&1";
$result = trim(shell_exec($cmd));
@unlink($tmpLocal);
if(strpos($result, 'OK') !== false) return "OK:MTA-EU-89.167.1.139";
// Fallback raw SMTP
return mta_eu_smtp_fallback($to, $subject, $html, $domain, $from_name, $from_email);
}
function mta_eu_smtp_fallback($to, $subject, $html, $domain, $from_name, $from_email) {
$mc_user = substr(md5(uniqid()), 0, 25);
$msg_id = "<$mc_user." . substr(uniqid(),0,14) . "@mail.$domain>";
$headers = "From: $from_name <$from_email>\r\n";
$headers .= "To: $to\r\nSubject: $subject\r\nDate: ".date('r')."\r\n";
$headers .= "Message-ID: $msg_id\r\nMIME-Version: 1.0\r\n";
$headers .= "List-Unsubscribe: <mailto:unsub@$domain?subject=unsubscribe>\r\n";
$headers .= "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n";
$headers .= "Content-Type: text/html; charset=utf-8\r\n";
$fp = @fsockopen('89.167.1.139', 25, $errno, $errstr, 10);
if(!$fp) return "FAIL:MTA-EU:$errstr";
$resp = fgets($fp, 512);
fputs($fp, "EHLO $domain\r\n"); $resp = fgets($fp, 512);
while(substr($resp,3,1)=='-') $resp = fgets($fp, 512);
fputs($fp, "MAIL FROM:<$from_email>\r\n"); $resp = fgets($fp, 512);
if(substr($resp,0,3)!='250') { fclose($fp); return "FAIL:MAIL_FROM:$resp"; }
fputs($fp, "RCPT TO:<$to>\r\n"); $resp = fgets($fp, 512);
if(substr($resp,0,3)!='250') { fclose($fp); return "FAIL:RCPT_TO:$resp"; }
fputs($fp, "DATA\r\n"); $resp = fgets($fp, 512);
if(substr($resp,0,3)!='354') { fclose($fp); return "FAIL:DATA:$resp"; }
fputs($fp, $headers . "\r\n" . $html . "\r\n.\r\n"); $resp = fgets($fp, 512);
fputs($fp, "QUIT\r\n"); fclose($fp);
return substr($resp,0,3)=='250' ? "OK:MTA-EU-FALLBACK" : "FAIL:$resp";
}
function o365_graph_send($to, $subject, $html, $from_name=null, $from_email=null) {
static $last_brand = [];
$db = pg_connect("host=localhost dbname=adx_system user=admin password=admin123");
pg_query($db, "SET search_path TO admin,public");
$html = process_template_vars($html, $to);
$subject = process_template_vars($subject, $to);
// Pick a sending account (graph_send = has mailbox)
$r = pg_query($db, "SELECT admin_email, tenant_id, app_id, app_secret, brand FROM office_accounts WHERE password_status='graph_send' ORDER BY RANDOM() LIMIT 1");
$acc = $r ? pg_fetch_assoc($r) : null;
if(!$acc) return "FAIL:no_graph_send_account";
// Get OAuth token
$ch = curl_init("https://login.microsoftonline.com/{$acc['tenant_id']}/oauth2/v2.0/token");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,CURLOPT_TIMEOUT=>10,
CURLOPT_POSTFIELDS=>http_build_query(['client_id'=>$acc['app_id'],'client_secret'=>$acc['app_secret'],
'scope'=>'https://graph.microsoft.com/.default','grant_type'=>'client_credentials'])]);
$token = json_decode(curl_exec($ch),true)['access_token'] ?? null;
if(!$token) return "FAIL:O365_TOKEN";
// Update displayName if brand changed for this account
$brand = $from_name ?: 'Service';
$key = $acc['admin_email'];
if(!isset($last_brand[$key]) || $last_brand[$key] !== $brand) {
$ch = curl_init("https://graph.microsoft.com/v1.0/users/" . urlencode($acc['admin_email']));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_CUSTOMREQUEST=>"PATCH", CURLOPT_TIMEOUT=>5,
CURLOPT_HTTPHEADER=>["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS=>json_encode(["displayName"=>$brand])]);
curl_exec($ch);
$last_brand[$key] = $brand;
usleep(500000); // 500ms for propagation
}
// Send
$payload = json_encode(['message'=>['subject'=>$subject,'body'=>['contentType'=>'HTML','content'=>$html],
'toRecipients'=>[['emailAddress'=>['address'=>$to]]]],'saveToSentItems'=>false]);
$ch = curl_init("https://graph.microsoft.com/v1.0/users/" . urlencode($acc['admin_email']) . "/sendMail");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,CURLOPT_TIMEOUT=>15,
CURLOPT_HTTPHEADER=>["Authorization: Bearer $token","Content-Type: application/json"],
CURLOPT_POSTFIELDS=>$payload]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
return $code==202 ? "OK:O365-GRAPH:{$brand}<{$acc['admin_email']}>" : "FAIL:O365:HTTP{$code}:" . substr($resp,0,200);
}
function process_template_vars($text, $to) {
$parts = explode('@', $to);
$local = $parts[0] ?? '';
$first = ucfirst(preg_replace('/[^a-z]/i', '', explode('.', $local)[0] ?? 'Customer'));
if(strlen($first)<2 || strlen($first)>20) $first = 'Customer';
$tid = 'T_' . bin2hex(random_bytes(8));
return str_replace(
['{tid}','{first_name}','{email}','{domain}','{date}','{unsubscribe}'],
[$tid, $first, $to, $parts[1]??'', date('d.m.Y'), '#'],
$text
);
}
function inject_tracking($html, $tid) { return inject_tracking_full($html, $tid, 0, ''); }
function inject_tracking_full($html, $tid, $offer_id=0, $offer_url='') {
$td = 'https://track.wevup.app';
$pixel = "<img src=\"{$td}/api/open.php?t={$tid}\" width=\"1\" height=\"1\" style=\"display:none\" />";
$html = strpos($html,'</body>')!==false ? str_replace('</body>',$pixel.'</body>',$html) : $html.$pixel;
if($offer_url) {
$click = "{$td}/api/click.php?t={$tid}&o={$offer_id}";
$html = preg_replace('/href=["\']([^"\']+)["\']/', 'href="'.$click.'"', $html, 1);
}
return $html;
}
if(!function_exists('detect_isp')) {
function detect_isp($email) {
$d = strtolower(substr(strrchr($email, "@"), 1));
$map = ['gmail.com'=>'gmail','googlemail.com'=>'gmail',
'outlook.com'=>'outlook','hotmail.com'=>'outlook','live.com'=>'outlook','msn.com'=>'outlook',
'yahoo.com'=>'yahoo','yahoo.de'=>'yahoo','yahoo.fr'=>'yahoo','ymail.com'=>'yahoo',
'gmx.de'=>'gmx','gmx.net'=>'gmx','gmx.at'=>'gmx','gmx.ch'=>'gmx',
'web.de'=>'web.de','t-online.de'=>'t-online','aol.com'=>'aol',
'icloud.com'=>'icloud','me.com'=>'icloud',
'protonmail.com'=>'protonmail','proton.me'=>'protonmail',
'mail.ru'=>'mail.ru','freenet.de'=>'freenet','arcor.de'=>'arcor',
'alice.it'=>'alice','tin.it'=>'alice','virgilio.it'=>'alice','tim.it'=>'alice',
'ziggo.nl'=>'ziggo','home.nl'=>'ziggo','chello.nl'=>'ziggo',
'videotron.ca'=>'videotron','videotron.qc.ca'=>'videotron',
'virginmedia.com'=>'virginmedia','ntlworld.com'=>'virginmedia',
'kpnmail.nl'=>'kpn','orange.fr'=>'orange','sfr.fr'=>'sfr',
'laposte.net'=>'laposte','libero.it'=>'libero',
'o2online.de'=>'o2','bluewin.ch'=>'bluewin',
];
return $map[$d] ?? explode('.', $d)[0];
}
}
function brain_log_send($isp, $method, $to, $status) {
$db = @pg_connect("host=localhost dbname=adx_system user=admin password=admin123");
if(!$db) return;
@pg_query($db, "SET search_path TO admin,public");
@pg_query_params($db, "INSERT INTO brain_method_scores (isp, method_name, tests_total, tests_delivered, last_test)
VALUES ($1, $2, 1, CASE WHEN $3='sent' THEN 1 ELSE 0 END, NOW())
ON CONFLICT (isp, method_name) DO UPDATE SET
tests_total = brain_method_scores.tests_total + 1,
tests_delivered = brain_method_scores.tests_delivered + CASE WHEN $3='sent' THEN 1 ELSE 0 END,
delivery_rate = ROUND(100.0 * (brain_method_scores.tests_delivered + CASE WHEN $3='sent' THEN 1 ELSE 0 END) / (brain_method_scores.tests_total + 1), 2),
last_test = NOW(), updated_at = NOW()", [$isp, $method, $status]);
}