Files
folio/src/mailer.php
T

182 lines
6.9 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 (defined('BASE_PATH') && !function_exists('smtpCfg')) {
require_once dirname(__DIR__) . '/src/SmtpSettings.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
{
if (!($opts['bypass_rate_limit'] ?? false)) {
[$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();
$_smtpRead = function_exists('smtpCfg');
$mail->Host = $_smtpRead ? smtpCfg('host', 'SMTP_HOST', 'localhost') : (string)env('SMTP_HOST', 'localhost');
$mail->Port = (int)($_smtpRead ? smtpCfg('port', 'SMTP_PORT', '587') : env('SMTP_PORT', '587'));
$_smtpUser = $_smtpRead ? smtpCfg('user', 'SMTP_USER') : (string)env('SMTP_USER', '');
$_smtpPass = $_smtpRead ? smtpCfg('pass', 'SMTP_PASS') : (string)env('SMTP_PASS', '');
$mail->SMTPAuth = ($_smtpUser !== '' || $_smtpPass !== '');
$mail->Username = $_smtpUser;
$mail->Password = $_smtpPass;
$secure = strtolower($_smtpRead ? smtpCfg('secure', 'SMTP_SECURE', 'tls') : (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 = $_smtpRead ? smtpCfg('from', 'SMTP_FROM', 'no-reply@varlog.a5l.fr') : (string)env('SMTP_FROM', 'no-reply@varlog.a5l.fr');
$fromName = $_smtpRead ? smtpCfg('from_name', 'SMTP_FROM_NAME', 'varlog') : (string)env('SMTP_FROM_NAME', 'varlog');
$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());
}
}