150 lines
6.0 KiB
PHP
150 lines
6.0 KiB
PHP
<?php
|
|
// projet : mug.a5l.fr
|
|
// fichier : includes/mailer.php
|
|
// version : 20251011
|
|
declare(strict_types=1);
|
|
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
use PHPMailer\PHPMailer\Exception;
|
|
|
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
if (!function_exists('env')) {
|
|
function env(string $key, ?string $default = null): ?string {
|
|
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') return (string)$_ENV[$key];
|
|
$v = getenv($key);
|
|
if ($v !== false && $v !== '') return (string)$v;
|
|
return $default;
|
|
}
|
|
}
|
|
if (!function_exists('db')) {
|
|
function db(): \PDO { return \App\Infrastructure\Database::get(); }
|
|
}
|
|
|
|
/**
|
|
* Anti-abus simple : 1 envoi / 5 min et 5 en 12 h par destinataire (status in ('sent','queued')).
|
|
*/
|
|
function mailer_can_send(string $email, int $coolMin = 5, int $maxPer12h = 5): array {
|
|
// bypass complet si désactivé
|
|
$enabled = (int) (env('SMTP_RATE_LIMIT_ENABLE', '1'));
|
|
if ($enabled === 0) return [true, ''];
|
|
|
|
$pdo = db();
|
|
|
|
// Cooldown (actif seulement si >0)
|
|
if ($coolMin > 0) {
|
|
$q1 = "SELECT 1 FROM journal_smtp
|
|
WHERE to_email = :e AND created_at >= NOW() - INTERVAL :cool
|
|
AND status IN ('sent','queued')
|
|
LIMIT 1";
|
|
$stmt = $pdo->prepare($q1);
|
|
$stmt->execute([':e'=>$email, ':cool'=>sprintf('%d minutes', $coolMin)]);
|
|
if ($stmt->fetchColumn()) return [false, "Un email vient d’être envoyé. Réessayez dans {$coolMin} min."];
|
|
}
|
|
|
|
// Plafond 12h (actif seulement si >0)
|
|
if ($maxPer12h > 0) {
|
|
$q2 = "SELECT COUNT(*) FROM journal_smtp
|
|
WHERE to_email = :e AND created_at >= NOW() - INTERVAL '12 hours'
|
|
AND status IN ('sent','queued')";
|
|
$stmt = $pdo->prepare($q2);
|
|
$stmt->execute([':e'=>$email]);
|
|
if ((int)$stmt->fetchColumn() >= $maxPer12h) return [false, 'Quota atteint. Réessayez plus tard.'];
|
|
}
|
|
|
|
return [true, ''];
|
|
}
|
|
|
|
|
|
/**
|
|
* Envoi immédiat SMTP avec PHPMailer + journalisation.
|
|
* @param string $to destinataire
|
|
* @param string $subject objet
|
|
* @param string $html corps HTML
|
|
* @param string|null $text corps texte brut (optionnel, auto-généré si null)
|
|
* @param array $opts ['reply_to'=>['email','name']]
|
|
*/
|
|
function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $text = null, array $opts = []): bool {
|
|
[$ok, $msg] = mailer_can_send($to, (int)env('SMTP_COOLDOWN_MINUTES', '5'), (int)env('SMTP_MAX_PER_12H', '5'));
|
|
if (!$ok) throw new RuntimeException($msg);
|
|
|
|
$pdo = db();
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$stmt = $pdo->prepare("INSERT INTO journal_smtp
|
|
(created_at, script_path, to_email, subject, content_html, content_text, status, ip, user_agent)
|
|
VALUES (NOW(), :script, :to, :subj, :html, :text, 'queued', :ip, :ua)
|
|
RETURNING id");
|
|
$stmt->execute([
|
|
':script' => ($_SERVER['SCRIPT_NAME'] ?? ''),
|
|
':to' => $to,
|
|
':subj' => $subject,
|
|
':html' => $html,
|
|
':text' => $text ?? trim(html_entity_decode(strip_tags($html), ENT_QUOTES)),
|
|
':ip' => ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''),
|
|
':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512),
|
|
]);
|
|
$rowId = (int)$stmt->fetchColumn();
|
|
$pdo->commit();
|
|
} catch (\Throwable $e) {
|
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
|
throw $e;
|
|
}
|
|
|
|
$mail = new PHPMailer(true);
|
|
try {
|
|
$mail->isSMTP();
|
|
$mail->Host = (string)env('SMTP_HOST', 'localhost');
|
|
$mail->Port = (int)env('SMTP_PORT', '587');
|
|
$mail->SMTPAuth = (env('SMTP_USER') || env('SMTP_PASS')) ? true : false;
|
|
$mail->Username = (string)env('SMTP_USER', '');
|
|
$mail->Password = (string)env('SMTP_PASS', '');
|
|
$secure = strtolower((string)env('SMTP_SECURE', 'tls'));
|
|
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
|
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
|
|
|
$mail->SMTPKeepAlive = true; // réutilise la connexion
|
|
$mail->Timeout = 30; // évite les blocages longs
|
|
$mail->SMTPOptions = ['ssl'=>['verify_peer'=>true,'verify_peer_name'=>true,'allow_self_signed'=>false]];
|
|
|
|
$mail->CharSet = 'UTF-8';
|
|
$mail->isHTML(true);
|
|
|
|
// Expéditeur
|
|
$from = (string)env('SMTP_FROM', 'no-reply@mug.a5l.fr');
|
|
$fromName = (string)env('SMTP_FROM_NAME', 'MUG');
|
|
$mail->setFrom($from, $fromName);
|
|
|
|
// Reply-To
|
|
if (!empty($opts['reply_to']) && is_array($opts['reply_to']) && filter_var($opts['reply_to'][0] ?? '', FILTER_VALIDATE_EMAIL)) {
|
|
$mail->addReplyTo($opts['reply_to'][0], $opts['reply_to'][1] ?? '');
|
|
} elseif ($rt = env('SMTP_REPLY_TO')) {
|
|
$mail->addReplyTo($rt, (string)env('SMTP_REPLY_TO_NAME', 'Support'));
|
|
}
|
|
|
|
// DKIM optionnel
|
|
if ($d = env('DKIM_DOMAIN')) {
|
|
$mail->DKIM_domain = $d;
|
|
$mail->DKIM_selector = (string)env('DKIM_SELECTOR', 'default');
|
|
$mail->DKIM_private = (string)env('DKIM_PRIVATE_KEY_PATH', '');
|
|
$mail->DKIM_passphrase = (string)env('DKIM_PASSPHRASE', '');
|
|
$mail->DKIM_identity = $from;
|
|
}
|
|
|
|
$mail->addAddress($to);
|
|
$mail->Subject = $subject;
|
|
$mail->Body = $html;
|
|
$mail->AltBody = $text ?? trim(html_entity_decode(strip_tags($html), ENT_QUOTES));
|
|
|
|
$mail->send();
|
|
|
|
$pdo->prepare("UPDATE journal_smtp SET status='sent', sent_at=NOW() WHERE id=:id")->execute([':id'=>$rowId]);
|
|
return true;
|
|
} catch (Exception $e) {
|
|
$pdo->prepare("UPDATE journal_smtp SET status='error', error_message=:err, sent_at=NOW() WHERE id=:id")
|
|
->execute([':id'=>$rowId, ':err'=>substr($e->getMessage(),0,1000)]);
|
|
throw new RuntimeException('Envoi email impossible: '.$e->getMessage());
|
|
}
|
|
}
|
|
|