'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; }