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

488 lines
23 KiB
PHP
Executable File

<?php
/**
* WARMUP ENGINE v2
* Manages warmup for ALL email send accounts across 21 providers
* - Auto-enrolls new accounts into warmup
* - Progressive volume increase per schedule
* - ISP-aware sending (right volume to right ISP)
* - Pause/resume on reputation drops
* - Graduate to production when ready
*/
require_once("/opt/wevads/config/credentials.php");
header('Content-Type: application/json');
$db = pg_connect("host=localhost dbname=adx_system user=admin password=".WEVADS_DB_PASS);
pg_query($db, "SET search_path TO admin");
$action = $_GET['action'] ?? $_POST['action'] ?? 'status';
switch($action) {
case 'enroll_all':
// Auto-enroll ALL pending/active send accounts into warmup
$r = pg_query($db, "
INSERT INTO warmup_accounts(email, account_type, daily_limit, current_day, status, created_at, sent_today)
SELECT e.email, e.provider,
CASE e.provider
WHEN 'office365' THEN 5
WHEN 'gmail' THEN 3
WHEN 'amazon_ses' THEN 10
WHEN 'sendgrid' THEN 5
WHEN 'mailgun' THEN 10
WHEN 'brevo' THEN 10
WHEN 'sparkpost' THEN 10
WHEN 'mailjet' THEN 5
WHEN 'postmark' THEN 3
WHEN 'elasticemail' THEN 5
WHEN 'turbosmtp' THEN 10
WHEN 'smtp2go' THEN 5
WHEN 'zoho' THEN 3
ELSE 5
END,
0,
CASE WHEN e.status='active' THEN 'warming' ELSE 'pending' END,
NOW(), 0
FROM email_send_accounts e
WHERE e.email NOT IN (SELECT email FROM warmup_accounts)
AND e.email IS NOT NULL AND e.email != ''
");
$cnt = pg_affected_rows($r);
echo json_encode([
'status' => 'success',
'enrolled' => $cnt,
'total_warmup' => pg_fetch_result(pg_query($db, "SELECT COUNT(*) FROM warmup_accounts"), 0)
]);
break;
case 'advance_day':
// Advance warmup day for all warming accounts + increase limits
// Called daily by cron
$schedules = [
// day => multiplier of base limit
0 => 1, 1 => 1.5, 2 => 2, 3 => 3, 4 => 4, 5 => 5,
7 => 7, 10 => 10, 14 => 15, 21 => 20, 28 => 30,
35 => 40, 42 => 60, 50 => 80, 60 => 100
];
// Get current warming accounts
$accounts = pg_fetch_all(pg_query($db, "SELECT id, email, account_type, current_day, daily_limit FROM warmup_accounts WHERE status='warming'"));
$advanced = 0;
$graduated = 0;
if ($accounts) {
foreach ($accounts as $acc) {
$new_day = $acc['current_day'] + 1;
// Calculate new limit based on provider base + day multiplier
$base = ['office365' => 5, 'gmail' => 3, 'amazon_ses' => 10, 'sendgrid' => 5, 'mailgun' => 10, 'brevo' => 10, 'sparkpost' => 10, 'mailjet' => 5, 'postmark' => 3, 'turbosmtp' => 10, 'smtp2go' => 5, 'zoho' => 3, 'hotmail' => 3, 'yahoo' => 3, 'gmx' => 3, 'webde' => 3, 't-online' => 2, 'libero' => 3, 'orange' => 3, 'sfr' => 3, 'elasticemail' => 5];
$b = $base[$acc['account_type']] ?? 5;
// Find best multiplier for current day
$mult = 1;
foreach ($schedules as $d => $m) {
if ($new_day >= $d) $mult = $m;
}
$new_limit = (int)($b * $mult);
// Max limits per provider
$maxes = ['office365' => 500, 'gmail' => 500, 'amazon_ses' => 10000, 'sendgrid' => 100, 'mailgun' => 300, 'brevo' => 300, 'sparkpost' => 500, 'mailjet' => 200, 'postmark' => 100, 'turbosmtp' => 500, 'smtp2go' => 200, 'zoho' => 200, 'hotmail' => 300, 'yahoo' => 200, 'gmx' => 200, 'webde' => 200, 't-online' => 100, 'libero' => 200, 'orange' => 100, 'sfr' => 100, 'elasticemail' => 100];
$max = $maxes[$acc['account_type']] ?? 200;
$new_limit = min($new_limit, $max);
// Graduate if at max limit for 7+ days
$status = 'warming';
if ($new_limit >= $max && $new_day >= 45) {
$status = 'graduated';
$graduated++;
// Update main send account
pg_query($db, "UPDATE email_send_accounts SET status='active', warmup_day=$new_day, daily_limit=$new_limit WHERE email='".pg_escape_string($db, $acc['email'])."'");
}
pg_query($db, "UPDATE warmup_accounts SET current_day=$new_day, daily_limit=$new_limit, sent_today=0, status='$status' WHERE id={$acc['id']}");
$advanced++;
}
}
echo json_encode([
'status' => 'success',
'advanced' => $advanced,
'graduated' => $graduated,
'date' => date('Y-m-d')
]);
break;
case 'daily_send_plan':
// Generate today's sending plan
$warming = pg_fetch_all(pg_query($db, "
SELECT wa.email, wa.account_type, wa.daily_limit, wa.sent_today, wa.current_day,
esa.smtp_host, esa.smtp_port
FROM warmup_accounts wa
LEFT JOIN email_send_accounts esa ON esa.email = wa.email
WHERE wa.status='warming' AND wa.sent_today < wa.daily_limit
ORDER BY wa.daily_limit DESC
"));
$plan = [];
$total_to_send = 0;
if ($warming) {
foreach ($warming as $w) {
$remaining = $w['daily_limit'] - $w['sent_today'];
$plan[] = [
'email' => $w['email'],
'provider' => $w['account_type'],
'to_send' => $remaining,
'day' => $w['current_day'],
'smtp' => $w['smtp_host'] . ':' . $w['smtp_port']
];
$total_to_send += $remaining;
}
}
echo json_encode([
'status' => 'success',
'accounts_to_send' => count($plan),
'total_emails_planned' => $total_to_send,
'plan' => array_slice($plan, 0, 50) // First 50
]);
break;
case 'pause':
$email = pg_escape_string($db, $_GET['email'] ?? '');
if ($email) {
pg_query($db, "UPDATE warmup_accounts SET status='paused' WHERE email='$email'");
echo json_encode(['status' => 'paused', 'email' => $email]);
}
break;
case 'resume':
$email = pg_escape_string($db, $_GET['email'] ?? '');
if ($email) {
pg_query($db, "UPDATE warmup_accounts SET status='warming' WHERE email='$email'");
echo json_encode(['status' => 'resumed', 'email' => $email]);
}
break;
case 'status':
$stats = pg_fetch_assoc(pg_query($db, "
SELECT
(SELECT COUNT(*) FROM warmup_accounts) as total_enrolled,
(SELECT COUNT(*) FROM warmup_accounts WHERE status='warming') as warming,
(SELECT COUNT(*) FROM warmup_accounts WHERE status='graduated') as graduated,
(SELECT COUNT(*) FROM warmup_accounts WHERE status='pending') as pending,
(SELECT COUNT(*) FROM warmup_accounts WHERE status='paused') as paused,
(SELECT SUM(daily_limit) FROM warmup_accounts WHERE status='warming') as warming_daily_capacity,
(SELECT SUM(sent_today) FROM warmup_accounts WHERE status='warming') as warming_sent_today,
(SELECT AVG(current_day) FROM warmup_accounts WHERE status='warming') as avg_warmup_day,
(SELECT MAX(current_day) FROM warmup_accounts WHERE status='warming') as max_warmup_day
"));
$by_provider = pg_fetch_all(pg_query($db, "
SELECT account_type, status, COUNT(*) as cnt, SUM(daily_limit) as capacity
FROM warmup_accounts GROUP BY account_type, status ORDER BY COUNT(*) DESC
"));
echo json_encode(['status' => 'success', 'warmup' => $stats, 'by_provider' => $by_provider]);
break;
case 'execute_warmup':
// EXECUTE WARMUP SENDS - The missing pipeline!
// Picks warming accounts, matches recipients, sends via O365
$batch_size = intval($_GET['batch'] ?? 10); // accounts per batch
$batch_size = min($batch_size, 50);
$dry_run = isset($_GET['dry_run']);
// 1. Get warming accounts with remaining quota
$accounts = pg_fetch_all(pg_query($db, "
SELECT wa.id, wa.email, wa.account_type, wa.daily_limit, wa.sent_today, wa.current_day,
esa.password, esa.smtp_host, esa.smtp_port
FROM warmup_accounts wa
JOIN email_send_accounts esa ON esa.email = wa.email
WHERE wa.status='warming' AND wa.sent_today < wa.daily_limit
AND esa.password IS NOT NULL AND esa.password != ''
ORDER BY wa.sent_today ASC
LIMIT $batch_size
"));
if (!$accounts) {
echo json_encode(['status' => 'success', 'message' => 'No accounts need sending', 'sent' => 0]);
break;
}
// 2. ISP mapping (contact isp → brain_config isp_target)
$isp_map = [
'gmx' => 'GMX', 'tonline' => 'T-ONLINE', 't-online' => 'T-ONLINE',
'hotmail' => 'OUTLOOK', 'outlook' => 'OUTLOOK', 'live' => 'OUTLOOK',
'gmail' => 'GMAIL', 'googlemail' => 'GMAIL',
'webde' => 'GMX', 'web.de' => 'GMX', // web.de uses GMX config
'ziggo' => 'ZIGGO', 'alice' => 'ALICE',
'videotron' => 'OUTLOOK' // default to Outlook config
];
// 3. Load ALL brain configs
$configs = [];
$res = pg_query($db, "SELECT * FROM brain_configs WHERE body_template IS NOT NULL");
while ($row = pg_fetch_assoc($res)) {
$configs[$row['isp_target']][] = $row;
}
// 4. ═══ QUALITY GUARD — Zero-tolerance offer selection ═══
require_once(__DIR__ . '/offer-quality-guard.php');
$qualityGuard = new OfferQualityGuard($db, 'culturellemejean.charity');
// Load ONLY approved offers with live links + good creatives
$approved_offers = [];
$boc_res = pg_query($db, "SELECT bc.offer_id, o.name, o.offer_url, o.affiliate_network_name,
o.countries,
an.sub_id_one, an.sub_id_two, an.sub_id_three,
l.value as link_url, bc.link_status, bc.good_creatives,
COALESCE(sc.requires_unsub_link, false) as requires_unsub
FROM admin.brain_offer_config bc
JOIN affiliate.offers o ON o.id = bc.offer_id
LEFT JOIN affiliate.affiliate_networks an ON an.id = o.affiliate_network_id
LEFT JOIN affiliate.links l ON l.offer_id = o.id AND l.type = 'preview'
LEFT JOIN admin.sponsor_config sc ON LOWER(o.affiliate_network_name) LIKE '%' || LOWER(sc.sponsor_name) || '%'
WHERE o.status = 'Activated' AND bc.is_active = true AND bc.is_approved = true
AND bc.link_status = 'live' AND bc.good_creatives > 0
ORDER BY bc.priority DESC");
while ($boc_row = pg_fetch_assoc($boc_res)) {
$approved_offers[] = $boc_row;
}
if (empty($approved_offers)) {
respond_json(['status'=>'error','message'=>'QUALITY_GUARD: No approved offers with live links. Run: php offer-quality-guard.php init']);
}
$trackDomain = 'culturellemejean.charity';
$total_sent = 0;
$total_failed = 0;
$results = [];
foreach ($accounts as $acc) {
$remaining = $acc['daily_limit'] - $acc['sent_today'];
$from_email = $acc['email'];
$from_domain = explode('@', $from_email)[1] ?? $trackDomain;
// Pick ISP targets to send to (spread across ISPs)
$target_isps = ['gmx', 'tonline', 'hotmail', 'webde'];
$sent_this_account = 0;
for ($i = 0; $i < $remaining && $i < 10; $i++) {
$target_isp = $target_isps[$i % count($target_isps)];
$brain_isp = $isp_map[$target_isp] ?? 'OUTLOOK';
// Get random recipient
$contact = pg_fetch_assoc(pg_query($db, "
SELECT email, COALESCE(NULLIF(first_name,''), 'Kunde') as fname,
COALESCE(NULLIF(last_name,''), '') as lname
FROM send_contacts
WHERE LOWER(isp) = '$target_isp' AND (status IS NULL OR status != 'bounced')
ORDER BY RANDOM() LIMIT 1
"));
if (!$contact) continue;
// ═══ QUALITY GUARD RULES ═══
// RULE 4: Check global + offer suppression
$recipient_lower = strtolower(trim($contact['email']));
$supp_check = pg_query($db, "SELECT 1 FROM admin.global_suppression WHERE email='".pg_escape_string($db,$recipient_lower)."' LIMIT 1");
if ($supp_check && pg_num_rows($supp_check) > 0) continue; // SKIP suppressed
// Select offer for this recipient (rotate among approved)
$offer = $approved_offers[array_rand($approved_offers)];
$offer['offer_url'] = $offer['link_url'] ?? $offer['offer_url'];
// RULE 4b: Check OFFER-SPECIFIC suppression (sponsor list per offer)
$osupp = pg_query($db, "SELECT 1 FROM admin.offer_suppression WHERE offer_id=".intval($offer['offer_id'])." AND email='".pg_escape_string($db,$recipient_lower)."' LIMIT 1");
if ($osupp && pg_num_rows($osupp) > 0) continue; // SKIP: on sponsor suppression list
// RULE 2+3: SMART SELECTION — Winner first, random fallback
$offer_creative = null;
// Try winner creative (from performance engine, min 20 sends)
$win_cr = pg_query($db, "SELECT cp.creative_id, c.value
FROM admin.creative_performance cp
JOIN affiliate.creatives c ON c.id = cp.creative_id
WHERE cp.offer_id=".intval($offer['offer_id'])."
AND cp.isp='".pg_escape_string($db,$brain_isp)."'
AND cp.is_winner = true AND cp.sends >= 20 AND c.quality_score >= 3 AND c.has_tracking_placeholders = true
".($offer['requires_unsub'] == 't' ? " AND encode(decode(c.value,'base64'),'escape') LIKE '%[unsub]%'" : "")."
LIMIT 1");
if ($win_cr && pg_num_rows($win_cr) > 0) {
$row = pg_fetch_assoc($win_cr);
$offer_creative = ['id' => $row['creative_id'], 'value' => $row['value']];
}
// Fallback: random creative for this offer
if (!$offer_creative) {
$cr_res = pg_query($db, "SELECT id, value FROM affiliate.creatives WHERE offer_id=".intval($offer['offer_id'])." AND status='Activated' AND quality_score >= 3 AND has_tracking_placeholders = true".($offer['requires_unsub'] == 't' ? " AND encode(decode(value,'base64'),'escape') LIKE '%[unsub]%'" : "")." ORDER BY RANDOM() LIMIT 1");
$offer_creative = ($cr_res && pg_num_rows($cr_res) > 0) ? pg_fetch_assoc($cr_res) : null;
}
// Subject: winner first (best open_rate), random fallback
$offer_subject = null;
$win_subj = pg_query($db, "SELECT sp.subject_text
FROM admin.subject_performance sp
WHERE sp.offer_id=".intval($offer['offer_id'])."
AND sp.isp='".pg_escape_string($db,$brain_isp)."'
AND sp.is_winner = true AND sp.sends >= 20
LIMIT 1");
if ($win_subj && pg_num_rows($win_subj) > 0) {
$offer_subject = pg_fetch_assoc($win_subj)['subject_text'];
}
if (!$offer_subject) {
$subj_res = pg_query($db, "SELECT value FROM affiliate.subjects WHERE offer_id=".intval($offer['offer_id'])." ORDER BY RANDOM() LIMIT 1");
$offer_subject = ($subj_res && pg_num_rows($subj_res) > 0) ? pg_fetch_assoc($subj_res)['value'] : null;
}
// From name: random
$fn_res = pg_query($db, "SELECT value FROM affiliate.from_names WHERE offer_id=".intval($offer['offer_id'])." ORDER BY RANDOM() LIMIT 1");
$offer_from_name = ($fn_res && pg_num_rows($fn_res) > 0) ? pg_fetch_assoc($fn_res)['value'] : null;
// Get brain config for this ISP
$cfg = null;
if (isset($configs[$brain_isp])) {
// Prefer OFFICE_365 method for O365 accounts
foreach ($configs[$brain_isp] as $c) {
if ($c['send_method'] === 'OFFICE_365') { $cfg = $c; break; }
}
if (!$cfg) $cfg = $configs[$brain_isp][0];
}
if (!$cfg && isset($configs['OUTLOOK'])) $cfg = $configs['OUTLOOK'][0]; // fallback
if (!$cfg) continue;
// Build email: prefer offer creative, fallback to brain config template
if ($offer_creative) {
$html = $offer_creative['value'];
// Decode base64 if needed
$decoded = @base64_decode($html);
if ($decoded && strlen($decoded) > 50 && strpos($decoded, '<') !== false) $html = $decoded;
} else {
$html = $cfg['body_template']; // Fallback to brain config
}
$subject = $offer_subject ?? $cfg['subject_template'] ?? 'Wichtige Mitteilung';
$tid = uniqid('wv_') . '_' . rand(100000,999999);
$msgId = $tid . '@' . $from_domain;
// Build offer URL with proper sub_ids
$base_offer_url = $offer['offer_url'] ?? 'https://track.cx3ads.com/click';
$network = $offer['affiliate_network_name'] ?? 'CX3 Ads';
$sub1 = 'wevads';
$sub2 = urlencode($tid . '|' . $brain_isp);
$sub3 = urlencode('wevads|' . $tid);
if (stripos($network, 'CX3') !== false) {
$full_offer_url = $base_offer_url . $sub1 . '&s2=' . $sub2 . '&s3=' . $sub3;
} else {
$sep = (strpos($base_offer_url, '?') !== false) ? '&' : '?';
$full_offer_url = $base_offer_url . $sep . 'sub1=' . $sub1 . '&sub2=' . $sub2 . '&sub3=' . $sub3;
}
// Replace ALL placeholders
$replacements = [
'[subject]' => $subject,
'[fname]' => $contact['fname'],
'[lname]' => $contact['lname'],
'[company]' => ucfirst(explode('.', $from_domain)[0]),
'[domain]' => $trackDomain,
'[url]' => 'https://' . $trackDomain . '/track.php?t=' . urlencode($tid) . '&e=click&u=' . rtrim(strtr(base64_encode($full_offer_url), '+/', '-_'), '='),
'[unsub]' => 'https://' . $trackDomain . '/track.php?t=' . urlencode($tid) . '&e=unsub',
'[open]' => 'https://' . $trackDomain . '/track.php?t=' . urlencode($tid) . '&e=open',
'[ID]' => rand(100000, 999999),
'[email_b64]' => rtrim(strtr(base64_encode($contact['email']), '+/', '-_'), '='),
];
foreach ($replacements as $k => $v) {
$html = str_replace($k, $v, $html);
$subject = str_replace($k, $v, $subject);
}
// HARD VALIDATION: Creative MUST contain tracking link after replacement
// If [url] was never in the creative, tracking URL wont be in final HTML
if (strpos($html, $trackDomain) === false || strpos($html, 'e=click') === false) {
error_log('QUALITY_BLOCK: Creative #' . ($offer_creative['id'] ?? 0) . ' offer #' . ($offer['offer_id'] ?? 0) . ' NO tracking link');
continue;
}
if ($dry_run) {
$total_sent++;
pg_query($db, "UPDATE unified_send_log_new SET offer_url_used='".pg_escape_string($db,$full_offer_url)."' , offer_id=".intval($offer['offer_id'] ?? 0).", creative_id=".intval($offer_creative['id'] ?? 0).", offer_name='".pg_escape_string($db,$offer['name'] ?? '')."', country='".pg_escape_string($db,$offer['countries'] ?? '')."' WHERE tracking_id='".pg_escape_string($db,$tid)."'");
$sent_this_account++;
continue;
}
// Send via local PMTA (port 25, no auth needed)
$errno = $errstr = '';
$smtp = @fsockopen('127.0.0.1', 25, $errno, $errstr, 5);
if (!$smtp) {
$total_failed++;
continue;
}
$resp = fgets($smtp, 512);
fputs($smtp, "EHLO $from_domain\r\n");
$resp = ''; while($line = fgets($smtp, 512)) { $resp .= $line; if(substr($line, 3, 1) == ' ') break; }
fputs($smtp, "MAIL FROM:<$from_email>\r\n"); $resp = fgets($smtp, 512);
fputs($smtp, "RCPT TO:<{$contact['email']}>\r\n"); $resp = fgets($smtp, 512);
fputs($smtp, "DATA\r\n"); $resp = fgets($smtp, 512);
// Build message — Exchange-like headers, NO X-Mailer (97% inbox)
$boundary = '----=_Part_' . uniqid();
$msg = "From: $from_email\r\n";
$msg .= "To: {$contact['email']}\r\n";
$msg .= "Subject: $subject\r\n";
$msg .= "Message-ID: <$msgId>\r\n";
$msg .= "Date: " . date('r') . "\r\n";
$msg .= "MIME-Version: 1.0\r\n";
$msg .= "Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n";
$msg .= "\r\n--$boundary\r\n";
$msg .= "Content-Type: text/plain; charset=utf-8\r\n\r\n";
$msg .= strip_tags($html) . "\r\n";
$msg .= "\r\n--$boundary\r\n";
$msg .= "Content-Type: text/html; charset=utf-8\r\n\r\n";
$msg .= $html . "\r\n";
$msg .= "\r\n--$boundary--\r\n";
$msg .= ".\r\n";
fputs($smtp, $msg);
$resp = fgets($smtp, 512);
fputs($smtp, "QUIT\r\n");
fclose($smtp);
$success = strpos($resp, '250') !== false;
// Log send
$resp_json = json_encode(['smtp_response' => trim($resp), 'method' => 'pmta']);
pg_query($db, "INSERT INTO unified_send_log_new(config_id,send_method,isp_target,from_email,to_email,subject,message_id,status,response_data,tracking_id)
VALUES({$cfg['id']},'pmta','$brain_isp','".pg_escape_string($db,$from_email)."','".pg_escape_string($db,$contact['email'])."','".pg_escape_string($db,$subject)."','$msgId','".($success?'sent':'failed')."','".pg_escape_string($db,$resp_json)."','".pg_escape_string($db,$tid)."')");
if ($success) {
$total_sent++;
pg_query($db, "UPDATE unified_send_log_new SET offer_url_used='".pg_escape_string($db,$full_offer_url)."' , offer_id=".intval($offer['offer_id'] ?? 0).", creative_id=".intval($offer_creative['id'] ?? 0).", offer_name='".pg_escape_string($db,$offer['name'] ?? '')."', country='".pg_escape_string($db,$offer['countries'] ?? '')."' WHERE tracking_id='".pg_escape_string($db,$tid)."'");
$sent_this_account++;
} else {
$total_failed++;
}
usleep(500000); // 0.5s between sends
}
// Update warmup account (skip in dry_run)
if ($sent_this_account > 0) {
if (!$dry_run) {
pg_query($db, "UPDATE warmup_accounts SET sent_today = sent_today + $sent_this_account, last_sent = NOW() WHERE id = {$acc['id']}");
}
$results[] = ['account' => $from_email, 'sent' => $sent_this_account];
}
}
echo json_encode([
'status' => 'success',
'mode' => $dry_run ? 'DRY_RUN' : 'LIVE',
'batch_size' => $batch_size,
'total_sent' => $total_sent,
'total_failed' => $total_failed,
'accounts_processed' => count($results),
'details' => array_slice($results, 0, 20),
'timestamp' => date('Y-m-d H:i:s')
]);
break;
}