<?php error_reporting(0); ini_set('display_errors', 0); set_time_limit(300); ignore_user_abort(true); session_start(); // Dossier de stockage des sessions $sessions_dir = sys_get_temp_dir() . '/mailer_sessions'; if (!is_dir($sessions_dir)) { mkdir($sessions_dir, 0777, true); } // Fonction pour envoyer un email via mail() function send_email($to, $subject, $text, $html, $sender_name, $sender_email) { $boundary = md5(uniqid(time())); $headers = ""; $headers .= "From: $sender_name <$sender_email>\r\n"; $headers .= "Reply-To: $sender_email\r\n"; $headers .= "MIME-Version: 1.0\r\n"; $headers .= "Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n"; $body = "--$boundary\r\n"; $body .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; $body .= ($text ?: strip_tags($html)) . "\r\n\r\n"; $body .= "--$boundary\r\n"; $body .= "Content-Type: text/html; charset=UTF-8\r\n\r\n"; $body .= $html . "\r\n\r\n"; $body .= "--$boundary--"; return @mail($to, $subject, $body, $headers, "-f$sender_email"); } // Lancer le worker via HTTP fire-and-forget (fsockopen) function fire_worker($campaign_id) { $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']; $dir = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/'); $path = "$dir/mailer_worker.php?campaign_id=" . urlencode($campaign_id); $port = ($scheme === 'https') ? 443 : 80; $prefix = ($scheme === 'https') ? 'ssl://' : ''; $fp = @fsockopen($prefix . $host, $port, $errno, $errstr, 2); if ($fp) { $req = "GET $path HTTP/1.1\r\n"; $req .= "Host: $host\r\n"; $req .= "Connection: close\r\n\r\n"; fwrite($fp, $req); // Ne pas attendre la réponse - fire and forget fclose($fp); return true; } return false; } // Vérifier si le worker background est actif (lock file récent) function is_worker_active($campaign_id, $sessions_dir) { $lock_file = "$sessions_dir/$campaign_id.lock"; if (file_exists($lock_file)) { $lock_data = @json_decode(file_get_contents($lock_file), true); if ($lock_data && (time() - $lock_data['time']) < 15) { return true; } } return false; } // Traitement des requêtes AJAX if ($_SERVER['REQUEST_METHOD'] === 'POST') { header('Content-Type: application/json'); $action = $_POST['action'] ?? ''; if ($action === 'init_campaign') { $campaign_id = uniqid('campaign_'); $sender_name = $_POST['sender_name'] ?? '4-72'; $sender_email = $_POST['sender_email'] ?? 'noreply@4-72.com.co'; $subject = $_POST['subject'] ?? 'Notification'; $html_template = $_POST['html_template'] ?? ''; $text_template = $_POST['text_template'] ?? ''; $emails_list = $_POST['emails_list'] ?? ''; $emails = array_values(array_filter(array_map('trim', explode("\n", $emails_list)))); if (count($emails) === 0) { echo json_encode(['status' => 'error', 'message' => 'Aucun email fourni']); exit; } $campaign_data = [ 'id' => $campaign_id, 'sender_name' => $sender_name, 'sender_email' => $sender_email, 'subject' => $subject, 'html_template' => $html_template, 'text_template' => $text_template, 'emails' => $emails, 'total' => count($emails), 'sent' => 0, 'errors' => 0, 'current_index' => 0, 'status' => 'running', 'started_at' => time(), 'last_update' => time() ]; file_put_contents("$sessions_dir/$campaign_id.json", json_encode($campaign_data)); // Tenter de lancer le worker en arrière-plan $worker_launched = fire_worker($campaign_id); echo json_encode([ 'status' => 'success', 'campaign_id' => $campaign_id, 'total_emails' => count($emails), 'worker' => $worker_launched ? 'launched' : 'fallback' ]); exit; } if ($action === 'process_batch') { $campaign_id = $_POST['campaign_id'] ?? ''; $campaign_file = "$sessions_dir/$campaign_id.json"; if (!file_exists($campaign_file)) { echo json_encode(['status' => 'error', 'message' => 'Campaign not found']); exit; } clearstatcache(); $campaign = json_decode(file_get_contents($campaign_file), true); if (!$campaign || $campaign['status'] !== 'running') { echo json_encode([ 'status' => 'success', 'sent' => $campaign['sent'] ?? 0, 'errors' => $campaign['errors'] ?? 0, 'total' => $campaign['total'] ?? 0, 'progress' => $campaign['total'] > 0 ? round(($campaign['current_index'] / $campaign['total']) * 100, 1) : 0, 'campaign_status' => $campaign['status'] ?? 'stopped' ]); exit; } if ($campaign['current_index'] >= $campaign['total']) { $campaign['status'] = 'completed'; file_put_contents($campaign_file, json_encode($campaign)); echo json_encode([ 'status' => 'success', 'sent' => $campaign['sent'], 'errors' => $campaign['errors'], 'total' => $campaign['total'], 'progress' => 100, 'campaign_status' => 'completed' ]); exit; } // Si le worker tourne déjà, juste retourner le statut sans envoyer if (is_worker_active($campaign_id, $sessions_dir)) { echo json_encode([ 'status' => 'success', 'sent' => $campaign['sent'], 'errors' => $campaign['errors'], 'total' => $campaign['total'], 'progress' => round(($campaign['current_index'] / $campaign['total']) * 100, 1), 'campaign_status' => $campaign['status'], 'worker_active' => true ]); exit; } // Worker pas actif → le navigateur prend le relai et envoie un batch $batch_size = 10; $start_index = $campaign['current_index']; $end_index = min($start_index + $batch_size, $campaign['total']); for ($i = $start_index; $i < $end_index; $i++) { $email = $campaign['emails'][$i]; if (filter_var($email, FILTER_VALIDATE_EMAIL)) { if (send_email($email, $campaign['subject'], $campaign['text_template'], $campaign['html_template'], $campaign['sender_name'], $campaign['sender_email'])) { $campaign['sent']++; } else { $campaign['errors']++; } } else { $campaign['errors']++; } usleep(30000); } $campaign['current_index'] = $end_index; $campaign['last_update'] = time(); if ($end_index >= $campaign['total']) { $campaign['status'] = 'completed'; } file_put_contents($campaign_file, json_encode($campaign)); echo json_encode([ 'status' => 'success', 'sent' => $campaign['sent'], 'errors' => $campaign['errors'], 'total' => $campaign['total'], 'progress' => round(($end_index / $campaign['total']) * 100, 1), 'campaign_status' => $campaign['status'], 'worker_active' => false ]); exit; } if ($action === 'get_status') { $campaign_id = $_POST['campaign_id'] ?? ''; $campaign_file = "$sessions_dir/$campaign_id.json"; if (!file_exists($campaign_file)) { echo json_encode(['status' => 'error', 'message' => 'Campaign not found']); exit; } clearstatcache(); $campaign = json_decode(file_get_contents($campaign_file), true); echo json_encode([ 'status' => 'success', 'sent' => $campaign['sent'], 'errors' => $campaign['errors'], 'total' => $campaign['total'], 'progress' => round(($campaign['current_index'] / $campaign['total']) * 100, 1), 'campaign_status' => $campaign['status'], 'worker_active' => is_worker_active($campaign_id, $sessions_dir) ]); exit; } if ($action === 'stop_campaign') { $campaign_id = $_POST['campaign_id'] ?? ''; $campaign_file = "$sessions_dir/$campaign_id.json"; if (file_exists($campaign_file)) { $campaign = json_decode(file_get_contents($campaign_file), true); $campaign['status'] = 'stopped'; file_put_contents($campaign_file, json_encode($campaign)); } echo json_encode(['status' => 'success']); exit; } if ($action === 'list_campaigns') { $campaigns = []; if (is_dir($sessions_dir)) { $files = glob("$sessions_dir/*.json"); foreach ($files as $file) { $campaign = @json_decode(file_get_contents($file), true); if ($campaign && ($campaign['status'] === 'running' || $campaign['status'] === 'stopped')) { $campaigns[] = [ 'id' => $campaign['id'], 'status' => $campaign['status'], 'sent' => $campaign['sent'], 'errors' => $campaign['errors'], 'total' => $campaign['total'], 'progress' => round(($campaign['current_index'] / $campaign['total']) * 100, 1), 'started_at' => $campaign['started_at'], 'worker_active' => is_worker_active($campaign['id'], $sessions_dir) ]; } } } echo json_encode([ 'status' => 'success', 'campaigns' => $campaigns ]); exit; } if ($action === 'resume_campaign') { $campaign_id = $_POST['campaign_id'] ?? ''; $campaign_file = "$sessions_dir/$campaign_id.json"; if (!file_exists($campaign_file)) { echo json_encode(['status' => 'error', 'message' => 'Campaign not found']); exit; } $campaign = json_decode(file_get_contents($campaign_file), true); $campaign['status'] = 'running'; $campaign['last_update'] = time(); file_put_contents($campaign_file, json_encode($campaign)); // Tenter de relancer le worker $worker_launched = fire_worker($campaign_id); echo json_encode([ 'status' => 'success', 'campaign_id' => $campaign_id, 'sent' => $campaign['sent'], 'errors' => $campaign['errors'], 'total' => $campaign['total'], 'progress' => round(($campaign['current_index'] / $campaign['total']) * 100, 1), 'worker' => $worker_launched ? 'launched' : 'fallback' ]); exit; } if ($action === 'upload_file') { if (!isset($_FILES['file'])) { echo json_encode(['status' => 'error', 'message' => 'No file uploaded']); exit; } $file = $_FILES['file']; $content = file_get_contents($file['tmp_name']); echo json_encode([ 'status' => 'success', 'content' => $content, 'lines' => count(array_filter(explode("\n", $content))) ]); exit; } } ?> <!DOCTYPE html> <html lang="fr"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>4-72 Mailer Pro</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Arial, sans-serif; background: linear-gradient(135deg, #1a1f71 0%, #2d35a8 100%); min-height: 100vh; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; } .header { background: white; padding: 30px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .header h1 { color: #1a1f71; margin-bottom: 5px; } .header p { color: #6b7280; font-size: 14px; } .content { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } .panel { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .panel h2 { color: #1a1f71; font-size: 18px; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 2px solid #e5e7eb; } .form-group { margin-bottom: 16px; } label { display: block; margin-bottom: 6px; color: #374151; font-weight: 600; font-size: 13px; } input[type="text"], input[type="email"], textarea, select { width: 100%; padding: 10px; border: 1px solid #e5e7eb; border-radius: 8px; font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; transition: border-color 0.3s; } input[type="text"]:focus, input[type="email"]:focus, textarea:focus, select:focus { outline: none; border-color: #1a1f71; box-shadow: 0 0 0 3px rgba(26, 31, 113, 0.1); } textarea { resize: vertical; min-height: 120px; font-family: 'Monaco', 'Courier New', monospace; font-size: 12px; } .file-input-wrapper { position: relative; overflow: hidden; display: inline-block; width: 100%; } .file-input-wrapper input[type="file"] { position: absolute; left: -9999px; } .file-input-label { display: block; padding: 10px; background: #f3f4f6; border: 2px dashed #d1d5db; border-radius: 8px; text-align: center; cursor: pointer; transition: all 0.3s; color: #6b7280; font-weight: 500; } .file-input-label:hover { background: #e5e7eb; border-color: #1a1f71; color: #1a1f71; } .file-input-label.loaded { background: #d1fae5; border-color: #6ee7b7; color: #065f46; } .button-group { display: flex; gap: 10px; margin-top: 24px; } button { flex: 1; padding: 12px; border: none; border-radius: 8px; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.3s; } .btn-primary { background: #1a1f71; color: white; } .btn-primary:hover:not(:disabled) { background: #0f1347; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(26, 31, 113, 0.3); } .btn-danger { background: #dc2626; color: white; } .btn-danger:hover:not(:disabled) { background: #b91c1c; } button:disabled { background: #9ca3af; cursor: not-allowed; opacity: 0.6; } .status-panel { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin-bottom: 20px; } .status-title { color: #1f2937; font-weight: 600; margin-bottom: 12px; } .progress-bar { width: 100%; height: 24px; background: #e5e7eb; border-radius: 12px; overflow: hidden; margin-bottom: 12px; } .progress-fill { height: 100%; background: linear-gradient(90deg, #1a1f71, #2d35a8); width: 0%; transition: width 0.3s; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: 600; } .stats { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; } .stat { background: white; padding: 12px; border-radius: 8px; text-align: center; border: 1px solid #e5e7eb; } .stat-value { font-size: 20px; font-weight: 700; color: #1a1f71; } .stat-label { font-size: 11px; color: #6b7280; margin-top: 4px; } .message { padding: 12px; border-radius: 8px; margin-bottom: 12px; display: none; } .message.success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; } .message.error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; } .message.info { background: #dbeafe; color: #0c4a6e; border: 1px solid #7dd3fc; } .message.show { display: block; } .campaigns-list { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; margin-bottom: 20px; } .campaign-item { background: white; padding: 12px; border-radius: 6px; margin-bottom: 8px; border-left: 4px solid #1a1f71; display: flex; justify-content: space-between; align-items: center; } .campaign-info { flex: 1; } .campaign-id { font-size: 12px; color: #6b7280; font-family: monospace; } .campaign-stats { font-size: 13px; color: #374151; margin-top: 4px; } .campaign-status { font-size: 11px; font-weight: 600; padding: 4px 8px; border-radius: 4px; margin-right: 8px; } .campaign-status.running { background: #dcfce7; color: #166534; } .campaign-status.stopped { background: #fee2e2; color: #991b1b; } .campaign-actions { display: flex; gap: 8px; } .btn-small { padding: 6px 12px; font-size: 12px; border-radius: 6px; border: none; cursor: pointer; font-weight: 600; transition: all 0.3s; } .btn-small.resume { background: #1a1f71; color: white; } .btn-small.resume:hover { background: #0f1347; } .btn-small.stop { background: #dc2626; color: white; } .btn-small.stop:hover { background: #b91c1c; } .global-controls { display: flex; gap: 10px; margin-bottom: 20px; } .global-controls button { flex: 1; padding: 12px; border: none; border-radius: 8px; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.3s; } .global-controls .btn-stop-all { background: #dc2626; color: white; } .global-controls .btn-stop-all:hover { background: #b91c1c; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3); } @media (max-width: 768px) { .content { grid-template-columns: 1fr; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>4-72 Mailer Pro</h1> <p>Envoi en masse avec contrôle total - Fonctionne en arrière-plan</p> </div> <!-- Campagnes en cours --> <div id="activeCampaigns" style="display: none;"> <div class="global-controls"> <button class="btn-stop-all" onclick="stopAllCampaigns()">⏹ Arrêter toutes les campagnes</button> </div> <div class="campaigns-list"> <div style="margin-bottom: 12px; font-weight: 600; color: #1f2937;">Campagnes en cours</div> <div id="campaignsList"></div> </div> </div> <div class="content"> <!-- Panneau de configuration --> <div class="panel"> <h2>Configuration</h2> <div class="form-group"> <label>Nom de l'expéditeur</label> <input type="text" id="senderName" value="4-72 Entregas" placeholder="Ex: 4-72 Entregas"> </div> <div class="form-group"> <label>Email de l'expéditeur</label> <input type="email" id="senderEmail" value="r.wolfe@murphybusinessboise.com" placeholder="Ex: sender@domain.com"> </div> <div class="form-group"> <label>Sujet de l'email</label> <input type="text" id="subject" value="Notificacion de su envio" placeholder="Ex: Notification"> </div> <div class="form-group"> <label>Template HTML</label> <textarea id="htmlTemplate" placeholder="Collez votre HTML ici..."></textarea> <small style="color: #9ca3af; margin-top: 4px; display: block;">Ou chargez un fichier HTML</small> </div> <div class="form-group"> <div class="file-input-wrapper"> <input type="file" id="htmlFile" accept=".html,.htm"> <label for="htmlFile" class="file-input-label">📄 Charger HTML</label> </div> </div> <div class="form-group"> <label>Template texte (alternatif)</label> <textarea id="textTemplate" placeholder="Version texte de l'email..."></textarea> </div> </div> <!-- Panneau d'envoi --> <div class="panel"> <h2>Liste d'emails</h2> <div id="message" class="message"></div> <div class="form-group"> <label>Emails (un par ligne)</label> <textarea id="emailsList" placeholder="email1@domain.com&#10;email2@domain.com&#10;email3@domain.com"></textarea> <small style="color: #9ca3af; margin-top: 4px; display: block;" id="emailCount">0 emails</small> </div> <div class="form-group"> <div class="file-input-wrapper"> <input type="file" id="emailsFile" accept=".txt,.csv"> <label for="emailsFile" class="file-input-label">📋 Charger fichier emails</label> </div> </div> <div class="button-group"> <button class="btn-primary" id="startBtn" onclick="startCampaign()">▶ Démarrer</button> <button class="btn-danger" id="stopBtn" onclick="stopCampaign()" disabled>⏹ Arrêter</button> </div> <div class="status-panel" id="statusPanel" style="display: none;"> <div class="status-title">Progression</div> <div class="progress-bar"> <div class="progress-fill" id="progressFill">0%</div> </div> <div class="stats"> <div class="stat"> <div class="stat-value" id="sentCount">0</div> <div class="stat-label">Envoyés</div> </div> <div class="stat"> <div class="stat-value" id="errorCount">0</div> <div class="stat-label">Erreurs</div> </div> <div class="stat"> <div class="stat-value" id="totalCount">0</div> <div class="stat-label">Total</div> </div> </div> </div> </div> </div> </div> <script> let campaignId = null; let processingInterval = null; let campaignStates = {}; // Charger les campagnes en cours au démarrage window.addEventListener('load', function() { loadActiveCampaigns(); setInterval(updateAllCampaignsStats, 3000); }); function updateAllCampaignsStats() { const formData = new FormData(); formData.append('action', 'list_campaigns'); fetch(window.location.href, { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (data.campaigns && data.campaigns.length > 0) { data.campaigns.forEach(c => { updateCampaignStats(c.id, c.sent, c.errors, c.total, c.progress); }); } }) .catch(() => {}); } function loadActiveCampaigns() { const formData = new FormData(); formData.append('action', 'list_campaigns'); fetch(window.location.href, { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (data.campaigns && data.campaigns.length > 0) { const container = document.getElementById('campaignsList'); data.campaigns.forEach(campaign => { const cid = campaign.id; let item = document.getElementById('campaign-' + cid); if (!item) { item = document.createElement('div'); item.id = 'campaign-' + cid; item.className = 'campaign-item'; container.appendChild(item); } campaignStates[cid] = campaign; const statusClass = campaign.status === 'running' ? 'running' : 'stopped'; const statusText = campaign.status === 'running' ? '▶ En cours' : '⏹ Arrêtée'; const workerInfo = campaign.worker_active ? ' (worker actif)' : ' (navigateur)'; item.innerHTML = '<div class="campaign-info">' + '<div class="campaign-id">' + cid + workerInfo + '</div>' + '<div class="campaign-stats">' + '<span id="sent-' + cid + '">' + campaign.sent + '</span>/' + '<span id="total-' + cid + '">' + campaign.total + '</span> envoyés • ' + '<span id="errors-' + cid + '">' + campaign.errors + '</span> erreurs • ' + '<span id="progress-' + cid + '">' + campaign.progress + '</span>%' + '</div></div>' + '<span class="campaign-status ' + statusClass + '">' + statusText + '</span>' + '<div class="campaign-actions">' + (campaign.status === 'stopped' ? '<button class="btn-small resume" onclick="resumeCampaign(\'' + cid + '\')">Reprendre</button>' : '') + '<button class="btn-small stop" onclick="stopCampaignById(\'' + cid + '\')">Arrêter</button>' + '</div>'; }); document.getElementById('activeCampaigns').style.display = 'block'; // Auto-attacher le polling à la première campagne running if (!campaignId) { const running = data.campaigns.find(c => c.status === 'running'); if (running) { campaignId = running.id; document.getElementById('startBtn').disabled = true; document.getElementById('stopBtn').disabled = false; document.getElementById('statusPanel').style.display = 'block'; document.getElementById('sentCount').textContent = running.sent; document.getElementById('errorCount').textContent = running.errors; document.getElementById('totalCount').textContent = running.total; document.getElementById('progressFill').style.width = running.progress + '%'; document.getElementById('progressFill').textContent = running.progress + '%'; processBatchPoll(); } } } else { document.getElementById('activeCampaigns').style.display = 'none'; campaignStates = {}; } }) .catch(e => console.error(e)); } function updateCampaignStats(cid, sent, errors, total, progress) { const sentEl = document.getElementById('sent-' + cid); const errorsEl = document.getElementById('errors-' + cid); const progressEl = document.getElementById('progress-' + cid); if (sentEl) sentEl.textContent = sent; if (errorsEl) errorsEl.textContent = errors; if (progressEl) progressEl.textContent = progress; campaignStates[cid] = { ...(campaignStates[cid] || {}), sent, errors, progress }; } // Polling hybride : appelle process_batch qui envoie OU retourne juste le statut function processBatchPoll() { if (!campaignId) return; const formData = new FormData(); formData.append('action', 'process_batch'); formData.append('campaign_id', campaignId); fetch(window.location.href, { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (data.status === 'success') { document.getElementById('sentCount').textContent = data.sent; document.getElementById('errorCount').textContent = data.errors; document.getElementById('progressFill').style.width = data.progress + '%'; document.getElementById('progressFill').textContent = data.progress + '%'; updateCampaignStats(campaignId, data.sent, data.errors, data.total, data.progress); if (data.campaign_status === 'completed') { showMessage('Campaign terminée! ' + data.sent + ' emails envoyés.', 'success'); document.getElementById('startBtn').disabled = false; document.getElementById('stopBtn').disabled = true; loadActiveCampaigns(); } else if (data.campaign_status === 'running') { // Si worker actif → poll lent (juste statut) // Si pas de worker → poll rapide (on envoie via navigateur) const delay = data.worker_active ? 2000 : 500; processingInterval = setTimeout(processBatchPoll, delay); } else { // stopped document.getElementById('startBtn').disabled = false; document.getElementById('stopBtn').disabled = true; loadActiveCampaigns(); } } else if (data.status === 'error') { showMessage('Erreur: ' + data.message, 'error'); } }) .catch(e => { processingInterval = setTimeout(processBatchPoll, 3000); }); } function resumeCampaign(cid) { const formData = new FormData(); formData.append('action', 'resume_campaign'); formData.append('campaign_id', cid); fetch(window.location.href, { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (data.status === 'success') { campaignId = data.campaign_id; document.getElementById('startBtn').disabled = true; document.getElementById('stopBtn').disabled = false; document.getElementById('statusPanel').style.display = 'block'; document.getElementById('sentCount').textContent = data.sent; document.getElementById('errorCount').textContent = data.errors; document.getElementById('totalCount').textContent = data.total; document.getElementById('progressFill').style.width = data.progress + '%'; document.getElementById('progressFill').textContent = data.progress + '%'; showMessage('Campaign reprise! ' + (data.worker === 'launched' ? 'Worker actif.' : 'Envoi via navigateur.'), 'success'); processBatchPoll(); loadActiveCampaigns(); } }) .catch(e => showMessage('Erreur: ' + e.message, 'error')); } // Charger HTML depuis fichier document.getElementById('htmlFile').addEventListener('change', function(e) { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(event) { document.getElementById('htmlTemplate').value = event.target.result; document.querySelector('label[for="htmlFile"]').classList.add('loaded'); document.querySelector('label[for="htmlFile"]').textContent = '✓ ' + file.name; }; reader.readAsText(file); } }); // Charger emails depuis fichier document.getElementById('emailsFile').addEventListener('change', function(e) { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(event) { document.getElementById('emailsList').value = event.target.result; updateEmailCount(); document.querySelector('label[for="emailsFile"]').classList.add('loaded'); document.querySelector('label[for="emailsFile"]').textContent = '✓ ' + file.name; }; reader.readAsText(file); } }); // Compter les emails document.getElementById('emailsList').addEventListener('input', updateEmailCount); function updateEmailCount() { const emails = document.getElementById('emailsList').value.split('\n').filter(e => e.trim()); document.getElementById('emailCount').textContent = emails.length + ' emails'; } function showMessage(text, type) { type = type || 'info'; const msg = document.getElementById('message'); msg.textContent = text; msg.className = 'message ' + type + ' show'; setTimeout(function() { msg.classList.remove('show'); }, 5000); } function startCampaign() { const senderName = document.getElementById('senderName').value.trim(); const senderEmail = document.getElementById('senderEmail').value.trim(); const subject = document.getElementById('subject').value.trim(); const htmlTemplate = document.getElementById('htmlTemplate').value.trim(); const textTemplate = document.getElementById('textTemplate').value.trim(); const emailsList = document.getElementById('emailsList').value.trim(); if (!senderName || !senderEmail || !subject || !htmlTemplate || !emailsList) { showMessage('Veuillez remplir tous les champs obligatoires', 'error'); return; } document.getElementById('startBtn').disabled = true; showMessage('Initialisation de la campagne...', 'info'); const formData = new FormData(); formData.append('action', 'init_campaign'); formData.append('sender_name', senderName); formData.append('sender_email', senderEmail); formData.append('subject', subject); formData.append('html_template', htmlTemplate); formData.append('text_template', textTemplate); formData.append('emails_list', emailsList); fetch(window.location.href, { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (data.status === 'success') { campaignId = data.campaign_id; document.getElementById('stopBtn').disabled = false; document.getElementById('statusPanel').style.display = 'block'; document.getElementById('totalCount').textContent = data.total_emails; const workerMsg = data.worker === 'launched' ? 'Worker lancé en arrière-plan!' : 'Envoi via navigateur (gardez la page ouverte).'; showMessage('Campaign démarrée! ' + workerMsg, 'success'); processBatchPoll(); loadActiveCampaigns(); } else { showMessage('Erreur: ' + (data.message || 'inconnue'), 'error'); document.getElementById('startBtn').disabled = false; } }) .catch(e => { showMessage('Erreur: ' + e.message, 'error'); document.getElementById('startBtn').disabled = false; }); } function stopCampaign() { if (!campaignId) return; stopCampaignById(campaignId); } function stopCampaignById(cid) { const formData = new FormData(); formData.append('action', 'stop_campaign'); formData.append('campaign_id', cid); fetch(window.location.href, { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (cid === campaignId) { clearTimeout(processingInterval); document.getElementById('startBtn').disabled = false; document.getElementById('stopBtn').disabled = true; } showMessage('Campaign arrêtée', 'info'); loadActiveCampaigns(); }) .catch(e => showMessage('Erreur: ' + e.message, 'error')); } function stopAllCampaigns() { if (!confirm('Arrêter TOUTES les campagnes en cours?')) return; const formData = new FormData(); formData.append('action', 'list_campaigns'); fetch(window.location.href, { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { if (data.campaigns && data.campaigns.length > 0) { let stopped = 0; const promises = data.campaigns.map(c => { const fd = new FormData(); fd.append('action', 'stop_campaign'); fd.append('campaign_id', c.id); return fetch(window.location.href, { method: 'POST', body: fd }) .then(r => r.json()) .then(res => { if (res.status === 'success') stopped++; }); }); Promise.all(promises).then(() => { clearTimeout(processingInterval); document.getElementById('startBtn').disabled = false; document.getElementById('stopBtn').disabled = true; showMessage(stopped + ' campagne(s) arrêtée(s)', 'success'); loadActiveCampaigns(); }); } }) .catch(e => showMessage('Erreur: ' + e.message, 'error')); } </script> </body> </html>