smtpCode, [550, 551, 552, 553, 554], true); } } /** * SMTP Relay — credentials stored in the DB, sending via PHPMailer. * * Responsibilities: * 1. CRUD on the singleton smtp_settings row. * 2. Send via PHPMailer SMTP (STARTTLS / SMTPS / plain). * 3. Probe credentials without sending any message (for validation on save). */ class SmtpRelay { // ----------------------------------------------------------------------- // DB operations // ----------------------------------------------------------------------- /** * Fetch current SMTP settings from the DB. * * @return array{host:string,port:int,encryption:string,username:string,password:string,from_email:string,from_name:string,notify_email:string} */ public static function getSettings(Database $db): array { $stmt = $db->getPDO()->query( 'SELECT host, port, encryption, username, password, from_email, from_name, notify_email FROM v_smtp_active LIMIT 1' ); $row = $stmt->fetch(); if ($row) { require_once __DIR__ . '/Crypto.php'; $row['password'] = Crypto::decrypt($row['password']); } return $row ?: [ 'host' => '', 'port' => 587, 'encryption' => 'tls', 'username' => '', 'password' => '', 'from_email' => '', 'from_name' => 'XAMXAM', 'notify_email' => '', ]; } /** * Return the address that should receive admin notification emails. * Uses notify_email when set, falls back to from_email. */ public static function getNotifyEmail(Database $db): string { $s = self::getSettings($db); $notify = trim($s['notify_email'] ?? ''); return $notify !== '' ? $notify : trim($s['from_email'] ?? ''); } /** * Upsert SMTP settings. * * @param array $data Keys: host, port, encryption, username, password, * from_email, from_name. Missing keys are left unchanged. */ public static function updateSettings(Database $db, array $data): void { $current = self::getSettings($db); $merged = array_merge($current, $data); $port = max(1, min(65535, (int)$merged['port'])); $encryption = in_array($merged['encryption'], ['tls', 'ssl', 'none'], true) ? $merged['encryption'] : 'tls'; $stmt = $db->getPDO()->prepare( 'UPDATE smtp_settings SET host = :host, port = :port, encryption = :encryption, username = :username, password = :password, from_email = :from_email, from_name = :from_name, notify_email = :notify_email, updated_at = CURRENT_TIMESTAMP WHERE id = 1' ); require_once __DIR__ . '/Crypto.php'; $stmt->execute([ ':host' => trim($merged['host']), ':port' => $port, ':encryption' => $encryption, ':username' => trim($merged['username']), ':password' => Crypto::encrypt($merged['password']), ':from_email' => trim($merged['from_email']), ':from_name' => trim($merged['from_name']), ':notify_email' => trim($merged['notify_email'] ?? ''), ]); } /** * Check whether the SMTP relay is fully configured. */ public static function isConfigured(Database $db): bool { $s = self::getSettings($db); return $s['host'] !== '' && $s['username'] !== '' && $s['from_email'] !== ''; } // ----------------------------------------------------------------------- // Credential probe (validates without sending any message) // ----------------------------------------------------------------------- /** * Test SMTP credentials without sending any message. * * Uses PHPMailer's smtpConnect() + smtpClose() to connect, * EHLO + optional STARTTLS, authenticate, then disconnect. * No MAIL FROM / RCPT TO / DATA — nothing lands in any mailbox. * * @return array{ok:bool, error:string, field:?string} */ public static function test(Database $db): array { $s = self::getSettings($db); if ($s['host'] === '') { return ['ok' => false, 'error' => 'Hôte SMTP non configuré.', 'field' => 'smtp_host']; } try { $mail = self::createMailer($s); $mail->SMTPDebug = 0; // Connect, authenticate, then close — no message sent if (!$mail->smtpConnect()) { $error = $mail->ErrorInfo; $isAuth = stripos($error, 'authenticat') !== false; $isLogin = stripos($error, 'login') !== false || stripos($error, 'username') !== false; $isPass = stripos($error, 'password') !== false; $isTls = stripos($error, 'tls') !== false || stripos($error, 'starttls') !== false; $field = $isAuth ? ($isPass ? 'smtp_password' : 'smtp_username') : ($isTls ? 'smtp_encryption' : null); throw new SmtpProbeException($error ?: 'Échec de la connexion SMTP.', $field); } $mail->smtpClose(); return ['ok' => true, 'error' => '', 'field' => null]; } catch (SmtpProbeException $e) { return ['ok' => false, 'error' => $e->getMessage(), 'field' => $e->field]; } catch (\Throwable $e) { return ['ok' => false, 'error' => $e->getMessage(), 'field' => null]; } } // ----------------------------------------------------------------------- // Send // ----------------------------------------------------------------------- /** * Build and return a PHPMailer instance configured from DB settings. */ private static function createMailer(array $s): PHPMailer { $mail = new PHPMailer(true); $mail->isSMTP(); $mail->CharSet = PHPMailer::CHARSET_UTF8; $mail->Host = $s['host']; $mail->Port = (int)$s['port']; $mail->Timeout = 15; if ($s['encryption'] === 'ssl') { $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; } elseif ($s['encryption'] === 'tls') { $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; } $mail->SMTPAuth = ($s['username'] !== ''); if ($mail->SMTPAuth) { $mail->Username = $s['username']; $mail->Password = $s['password']; $mail->AuthType = 'PLAIN'; // Let PHPMailer try PLAIN then LOGIN } $mail->setFrom($s['from_email'], $s['from_name'] ?? 'XAMXAM'); return $mail; } /** * Send an e-mail using the stored SMTP credentials. * * @param string $to Recipient e-mail address * @param string $subject Subject line * @param string $body HTML body * @param string $plain Plain-text alternative (optional) * @return bool True on successful delivery acceptance; false on failure */ public static function send( Database $db, string $to, string $subject, string $body, string $plain = '' ): bool { $s = self::getSettings($db); if ($s['from_email'] === '') { error_log('[SmtpRelay] send() aborted — no from_email configured'); return false; } try { $mail = self::createMailer($s); $mail->addAddress($to); $mail->Subject = $subject; $mail->Body = $body; $mail->isHTML(true); if ($plain !== '') { $mail->AltBody = $plain; } $mail->send(); return true; } catch (SmtpSendException $e) { error_log('[SmtpRelay] ' . $e->getMessage()); throw $e; // propagate structured exception so callers can react } catch (PHPMailerException $e) { error_log('[SmtpRelay] ' . $e->getMessage()); return false; } catch (\Throwable $e) { error_log('[SmtpRelay] ' . $e->getMessage()); return false; } } // ----------------------------------------------------------------------- // Queue stub // ----------------------------------------------------------------------- /** * Queue (persist) an e-mail for deferred sending. * Stub — will create a `mail_queue` table in a future migration. */ public static function queue( Database $db, string $to, string $subject, string $body, string $plain = '' ): void { // TODO: INSERT INTO mail_queue … } }