488 lines
23 KiB
PHP
Executable File
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;
|
|
|
|
|
|
}
|