<?php
// ========== НАСТРОЙКИ СМТП (ВАШ ДОМЕН) ==========
define('SMTP_HOST', 'smtp.yandex.ru'); // Ваш SMTP-сервер
define('SMTP_PORT', 465); // 465 (SSL) или 587 (TLS)
define('SMTP_SECURE', 'ssl'); // 'ssl' или 'tls'
define('SMTP_AUTH', true);
define('SMTP_USER', 'no-reply@ваш-домен.ru');
define('SMTP_PASS', 'пароль_приложения');
define('FROM_EMAIL', 'no-reply@ваш-домен.ru');
define('FROM_NAME', 'Рассылка сайта');
// ========== ИНИЦИАЛИЗАЦИЯ SQLite ==========
$db_file = 'subscribers.db';
$pdo = new PDO("sqlite:$db_file");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Таблица подписчиков
$pdo->exec("CREATE TABLE IF NOT EXISTS subscribers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT,
active INTEGER DEFAULT 1
)");
// Таблица очереди писем
$pdo->exec("CREATE TABLE IF NOT EXISTS mail_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
to_email TEXT NOT NULL,
to_name TEXT,
subject TEXT,
html_body TEXT,
status TEXT DEFAULT 'pending',
error TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
// ========== ОБРАБОТКА ДЕЙСТВИЙ ==========
$message = '';
// Добавление подписчика через форму
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_subscriber'])) {
$email = trim($_POST['email']);
$name = trim($_POST['name']);
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
try {
$stmt = $pdo->prepare("INSERT INTO subscribers (email, name) VALUES (?, ?)");
$stmt->execute([$email, $name]);
$message = "✅ Подписчик $email добавлен";
} catch (PDOException $e) {
$message = "⚠️ Ошибка: email уже существует";
}
} else {
$message = "❌ Неверный email";
}
}
// Загрузка CSV
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] === UPLOAD_ERR_OK) {
$file = fopen($_FILES['csv_file']['tmp_name'], 'r');
$added = 0;
while (($row = fgetcsv($file)) !== false) {
$email = trim($row[0]);
$name = isset($row[1]) ? trim($row[1]) : '';
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
try {
$stmt = $pdo->prepare("INSERT OR IGNORE INTO subscribers (email, name) VALUES (?, ?)");
$stmt->execute([$email, $name]);
if ($stmt->rowCount()) $added++;
} catch (Exception $e) {}
}
}
fclose($file);
$message = "📥 Импортировано $added подписчиков";
}
// Создание рассылки (постановка в очередь)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_campaign'])) {
$subject = $_POST['subject'];
$html = $_POST['html_content'];
// Получаем всех активных подписчиков
$subs = $pdo->query("SELECT email, name FROM subscribers WHERE active = 1")->fetchAll(PDO::FETCH_ASSOC);
$queued = 0;
$stmt = $pdo->prepare("INSERT INTO mail_queue (to_email, to_name, subject, html_body) VALUES (?, ?, ?, ?)");
foreach ($subs as $sub) {
$personalized = str_replace('{name}', htmlspecialchars($sub['name'] ?: 'друг'), $html);
$stmt->execute([$sub['email'], $sub['name'], $subject, $personalized]);
$queued++;
}
$message = "✉️ $queued писем поставлено в очередь";
}
// Принудительная отправка очереди (вызывается вручную или по cron)
if (isset($_GET['process_queue']) && $_GET['process_queue'] === '1') {
$pending = $pdo->query("SELECT * FROM mail_queue WHERE status = 'pending' LIMIT 10")->fetchAll(PDO::FETCH_ASSOC);
$sent = 0;
foreach ($pending as $mail) {
$result = smtp_send(
$mail['to_email'],
$mail['to_name'],
$mail['subject'],
$mail['html_body']
);
if ($result['success']) {
$pdo->prepare("UPDATE mail_queue SET status = 'sent' WHERE id = ?")->execute([$mail['id']]);
$sent++;
} else {
$pdo->prepare("UPDATE mail_queue SET status = 'failed', error = ? WHERE id = ?")->execute([$result['error'], $mail['id']]);
}
}
$message = "📨 Отправлено $sent из " . count($pending) . " писем";
}
// Функция отправки через SMTP
function smtp_send($to, $to_name, $subject, $html_body) {
$text_body = strip_tags($html_body);
$boundary = md5(uniqid(time()));
$headers = "MIME-Version: 1.0\r\n";
$headers .= "From: " . FROM_NAME . " <" . FROM_EMAIL . ">\r\n";
$headers .= "Reply-To: " . FROM_EMAIL . "\r\n";
$headers .= "Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n";
$message = "--$boundary\r\n";
$message .= "Content-Type: text/plain; charset=utf-8\r\n\r\n$text_body\r\n";
$message .= "--$boundary\r\n";
$message .= "Content-Type: text/html; charset=utf-8\r\n\r\n$html_body\r\n";
$message .= "--$boundary--";
$to_address = $to_name ? "$to_name <$to>" : $to;
$subject_enc = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$host = SMTP_HOST;
$port = SMTP_PORT;
$secure = SMTP_SECURE;
$user = SMTP_USER;
$pass = SMTP_PASS;
if ($secure === 'ssl') {
$host = 'ssl://' . $host;
$port = $port ?: 465;
}
$smtp = fsockopen($host, $port, $errno, $errstr, 30);
if (!$smtp) return ['success' => false, 'error' => "Не удалось подключиться: $errstr"];
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '220') return ['success' => false, 'error' => "SMTP: $response"];
fputs($smtp, "EHLO " . gethostname() . "\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '250') return ['success' => false, 'error' => "EHLO: $response"];
if ($secure === 'tls') {
fputs($smtp, "STARTTLS\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '220') return ['success' => false, 'error' => "STARTTLS: $response"];
stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
fputs($smtp, "EHLO " . gethostname() . "\r\n");
$response = fgets($smtp, 515);
}
if (SMTP_AUTH) {
fputs($smtp, "AUTH LOGIN\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '334') return ['success' => false, 'error' => "AUTH: $response"];
fputs($smtp, base64_encode($user) . "\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '334') return ['success' => false, 'error' => "USER: $response"];
fputs($smtp, base64_encode($pass) . "\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '235') return ['success' => false, 'error' => "PASS: $response"];
}
fputs($smtp, "MAIL FROM: <" . FROM_EMAIL . ">\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '250') return ['success' => false, 'error' => "MAIL FROM: $response"];
fputs($smtp, "RCPT TO: <$to>\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '250') return ['success' => false, 'error' => "RCPT TO: $response"];
fputs($smtp, "DATA\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '354') return ['success' => false, 'error' => "DATA: $response"];
fputs($smtp, "Subject: $subject_enc\r\n$headers\r\n$message\r\n.\r\n");
$response = fgets($smtp, 515);
if (substr($response, 0, 3) != '250') return ['success' => false, 'error' => "SEND: $response"];
fputs($smtp, "QUIT\r\n");
fclose($smtp);
return ['success' => true, 'error' => null];
}
// Статистика для интерфейса
$subscribers_count = $pdo->query("SELECT COUNT(*) FROM subscribers WHERE active = 1")->fetchColumn();
$queue_count = $pdo->query("SELECT COUNT(*) FROM mail_queue WHERE status = 'pending'")->fetchColumn();
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email рассыльщик — ваш домен</title>
<style>
* { box-sizing: border-box; font-family: system-ui, 'Segoe UI', Roboto, sans-serif; }
body { background: #f1f5f9; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.card { background: white; border-radius: 24px; padding: 24px; margin-bottom: 24px; box-shadow: 0 8px 20px rgba(0,0,0,0.05); }
h1, h2 { margin-top: 0; color: #0f172a; }
.stats { display: flex; gap: 20px; margin-bottom: 24px; flex-wrap: wrap; }
.stat { background: white; border-radius: 20px; padding: 20px; flex: 1; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.stat-number { font-size: 2.5rem; font-weight: bold; color: #3b82f6; }
.stat-label { color: #475569; margin-top: 8px; }
.btn { background: #3b82f6; color: white; border: none; padding: 10px 20px; border-radius: 40px; cursor: pointer; font-weight: 600; transition: 0.2s; }
.btn:hover { background: #2563eb; }
.btn-outline { background: white; border: 1px solid #cbd5e1; color: #1e293b; }
.btn-outline:hover { background: #f8fafc; }
input, textarea, .file-label { width: 100%; padding: 12px; border: 1px solid #cbd5e1; border-radius: 16px; margin-top: 8px; margin-bottom: 16px; font-size: 1rem; }
textarea { font-family: monospace; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.message { background: #e0f2fe; padding: 12px; border-radius: 16px; margin-bottom: 20px; }
hr { margin: 24px 0; border: none; border-top: 1px solid #e2e8f0; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #e2e8f0; }
th { background: #f8fafc; }
.badge { background: #f1f5f9; padding: 4px 10px; border-radius: 40px; font-size: 0.8rem; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>📧 Рассыльщик на вашем домене</h1>
<p>Работает через ваш SMTP (<strong><?= htmlspecialchars(SMTP_HOST) ?></strong>). Письма подписываются DKIM/SPF, если вы настроили DNS.</p>
<div class="stats">
<div class="stat"><div class="stat-number"><?= $subscribers_count ?></div><div class="stat-label">Активных подписчиков</div></div>
<div class="stat"><div class="stat-number"><?= $queue_count ?></div><div class="stat-label">Писем в очереди</div></div>
<div class="stat"><div class="stat-number"><a href="?process_queue=1" class="btn btn-outline" style="text-decoration:none;">▶️ Отправить 10</a></div><div class="stat-label">Запустить очередь вручную</div></div>
</div>
<?php if ($message): ?><div class="message"><?= $message ?></div><?php endif; ?>
</div>
<div class="grid-2">
<!-- Управление подписчиками -->
<div class="card">
<h2>📋 Подписчики</h2>
<form method="POST" enctype="multipart/form-data">
<label>CSV файл (email,name)</label>
<input type="file" name="csv_file" accept=".csv">
<button type="submit" class="btn">📤 Импортировать CSV</button>
</form>
<hr>
<form method="POST">
<input type="email" name="email" placeholder="Email" required>
<input type="text" name="name" placeholder="Имя (необязательно)">
<button type="submit" name="add_subscriber" class="btn">➕ Добавить подписчика</button>
</form>
<hr>
<div style="max-height: 300px; overflow-y: auto;">
<table>
<thead><tr><th>Email</th><th>Имя</th></tr></thead>
<tbody>
<?php
$subs = $pdo->query("SELECT email, name FROM subscribers WHERE active = 1 LIMIT 20")->fetchAll();
foreach ($subs as $s): ?>
<tr><td><?= htmlspecialchars($s['email']) ?></td><td><?= htmlspecialchars($s['name']) ?></td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Создание рассылки -->
<div class="card">
<h2>✍️ Новая рассылка</h2>
<form method="POST">
<input type="text" name="subject" placeholder="Тема письма" required>
<textarea name="html_content" rows="10" placeholder="HTML-содержимое письма. Можно использовать {name} для подстановки имени." required></textarea>
<button type="submit" name="create_campaign" class="btn">🚀 Поставить в очередь (всем подписчикам)</button>
</form>
<div class="badge" style="margin-top: 16px;">💡 Совет: используйте переменную <code>{name}</code> — она заменится на имя подписчика.</div>
</div>
</div>
<!-- Лог последних отправленных писем -->
<div class="card">
<h2>📜 Последние письма (очередь)</h2>
<table style="width:100%">
<thead><tr><th>Кому</th><th>Тема</th><th>Статус</th><th>Ошибка</th></tr></thead>
<tbody>
<?php
$logs = $pdo->query("SELECT to_email, subject, status, error FROM mail_queue ORDER BY id DESC LIMIT 15")->fetchAll();
foreach ($logs as $log): ?>
<tr>
<td><?= htmlspecialchars($log['to_email']) ?></td>
<td><?= htmlspecialchars($log['subject']) ?></td>
<td><?= $log['status'] === 'sent' ? '✅ Отправлено' : ($log['status'] === 'pending' ? '⏳ В очереди' : '❌ Ошибка') ?></td>
<td><?= htmlspecialchars($log['error']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="badge" style="margin-top: 12px;">🔁 Очередь обрабатывается автоматически при заходе на страницу ?process_queue=1 или вы можете настроить CRON: <code>wget -q -O /dev/null "https://ваш-домен/index.php?process_queue=1"</code></p>
</div>
</div>
</body>
</html>