Initial dev export (exclude uploads/runtime)

This commit is contained in:
AudioCore Bot
2026-03-04 20:46:11 +00:00
commit b2afadd539
120 changed files with 20410 additions and 0 deletions

226
core/services/Mailer.php Normal file
View File

@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace Core\Services;
class Mailer
{
public static function send(string $to, string $subject, string $html, array $settings): array
{
$host = (string)($settings['smtp_host'] ?? '');
$port = (int)($settings['smtp_port'] ?? 587);
$user = (string)($settings['smtp_user'] ?? '');
$pass = (string)($settings['smtp_pass'] ?? '');
$encryption = strtolower((string)($settings['smtp_encryption'] ?? 'tls'));
$fromEmail = (string)($settings['smtp_from_email'] ?? '');
$fromName = (string)($settings['smtp_from_name'] ?? '');
if ($fromEmail === '') {
$fromEmail = $user !== '' ? $user : 'no-reply@localhost';
}
$fromHeader = $fromName !== '' ? "{$fromName} <{$fromEmail}>" : $fromEmail;
if ($host === '') {
$headers = [
'MIME-Version: 1.0',
'Content-type: text/html; charset=utf-8',
"From: {$fromHeader}",
];
$ok = mail($to, $subject, $html, implode("\r\n", $headers));
return ['ok' => $ok, 'error' => $ok ? '' : 'mail() failed', 'debug' => 'transport=mail()'];
}
$remote = $encryption === 'ssl' ? "ssl://{$host}:{$port}" : "{$host}:{$port}";
$socket = stream_socket_client($remote, $errno, $errstr, 10);
if (!$socket) {
return ['ok' => false, 'error' => "SMTP connect failed: {$errstr}", 'debug' => "connect={$remote} errno={$errno} err={$errstr}"];
}
$debug = [];
$read = function () use ($socket): string {
$data = '';
while (!feof($socket)) {
$line = fgets($socket, 515);
if ($line === false) {
break;
}
$data .= $line;
if (isset($line[3]) && $line[3] === ' ') {
break;
}
}
return $data;
};
$send = function (string $command) use ($socket, $read): string {
fwrite($socket, $command . "\r\n");
return $read();
};
$resp = $read();
$debug[] = 'S: ' . trim($resp);
if (!self::isOkResponse($resp)) {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP greeting failed', 'debug' => implode("\n", $debug)];
}
$resp = $send("EHLO localhost");
$debug[] = 'C: EHLO localhost';
$debug[] = 'S: ' . trim($resp);
if (!self::isOkResponse($resp)) {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP EHLO failed', 'debug' => implode("\n", $debug)];
}
$authCaps = self::parseAuthCapabilities($resp);
if ($encryption === 'tls') {
$resp = $send("STARTTLS");
$debug[] = 'C: STARTTLS';
$debug[] = 'S: ' . trim($resp);
if (substr(trim($resp), 0, 3) !== '220') {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP STARTTLS failed', 'debug' => implode("\n", $debug)];
}
if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP TLS negotiation failed', 'debug' => implode("\n", $debug)];
}
$resp = $send("EHLO localhost");
$debug[] = 'C: EHLO localhost';
$debug[] = 'S: ' . trim($resp);
if (!self::isOkResponse($resp)) {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP EHLO after TLS failed', 'debug' => implode("\n", $debug)];
}
$authCaps = self::parseAuthCapabilities($resp);
}
if ($user !== '' && $pass !== '') {
$authOk = false;
$authErrors = [];
// Prefer advertised AUTH mechanisms when available.
if (in_array('PLAIN', $authCaps, true)) {
$authResp = $send("AUTH PLAIN " . base64_encode("\0{$user}\0{$pass}"));
$debug[] = 'C: AUTH PLAIN [credentials]';
$debug[] = 'S: ' . trim($authResp);
if (substr(trim($authResp), 0, 3) === '235') {
$authOk = true;
} else {
$authErrors[] = 'PLAIN rejected';
}
}
if (!$authOk && (in_array('LOGIN', $authCaps, true) || !$authCaps)) {
$resp = $send("AUTH LOGIN");
$debug[] = 'C: AUTH LOGIN';
$debug[] = 'S: ' . trim($resp);
if (substr(trim($resp), 0, 3) === '334') {
$resp = $send(base64_encode($user));
$debug[] = 'C: [username]';
$debug[] = 'S: ' . trim($resp);
if (substr(trim($resp), 0, 3) === '334') {
$resp = $send(base64_encode($pass));
$debug[] = 'C: [password]';
$debug[] = 'S: ' . trim($resp);
if (substr(trim($resp), 0, 3) === '235') {
$authOk = true;
} else {
$authErrors[] = 'LOGIN password rejected';
}
} else {
$authErrors[] = 'LOGIN username rejected';
}
} else {
$authErrors[] = 'LOGIN command rejected';
}
}
if (!$authOk) {
fclose($socket);
$err = $authErrors ? implode(', ', $authErrors) : 'No supported AUTH method';
return ['ok' => false, 'error' => 'SMTP authentication failed: ' . $err, 'debug' => implode("\n", $debug)];
}
}
$resp = $send("MAIL FROM:<{$fromEmail}>");
$debug[] = "C: MAIL FROM:<{$fromEmail}>";
$debug[] = 'S: ' . trim($resp);
if (!self::isOkResponse($resp)) {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP MAIL FROM failed', 'debug' => implode("\n", $debug)];
}
$resp = $send("RCPT TO:<{$to}>");
$debug[] = "C: RCPT TO:<{$to}>";
$debug[] = 'S: ' . trim($resp);
if (!self::isOkResponse($resp)) {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP RCPT TO failed', 'debug' => implode("\n", $debug)];
}
$resp = $send("DATA");
$debug[] = 'C: DATA';
$debug[] = 'S: ' . trim($resp);
if (substr(trim($resp), 0, 3) !== '354') {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP DATA failed', 'debug' => implode("\n", $debug)];
}
$headers = [
"From: {$fromHeader}",
"To: {$to}",
"Subject: {$subject}",
"MIME-Version: 1.0",
"Content-Type: text/html; charset=utf-8",
];
$message = implode("\r\n", $headers) . "\r\n\r\n" . $html . "\r\n.";
fwrite($socket, $message . "\r\n");
$resp = $read();
$debug[] = 'C: [message body]';
$debug[] = 'S: ' . trim($resp);
if (!self::isOkResponse($resp)) {
fclose($socket);
return ['ok' => false, 'error' => 'SMTP message rejected', 'debug' => implode("\n", $debug)];
}
$resp = $send("QUIT");
$debug[] = 'C: QUIT';
$debug[] = 'S: ' . trim($resp);
fclose($socket);
return ['ok' => true, 'error' => '', 'debug' => implode("\n", $debug)];
}
private static function isOkResponse(string $response): bool
{
$code = substr(trim($response), 0, 3);
if ($code === '') {
return false;
}
return $code[0] === '2' || $code[0] === '3';
}
/**
* @return string[]
*/
private static function parseAuthCapabilities(string $ehloResponse): array
{
$caps = [];
foreach (preg_split('/\r?\n/', $ehloResponse) as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
$line = preg_replace('/^\d{3}[ -]/', '', $line) ?? $line;
if (stripos($line, 'AUTH ') === 0) {
$parts = preg_split('/\s+/', substr($line, 5)) ?: [];
foreach ($parts as $p) {
$p = strtoupper(trim($p));
if ($p !== '') {
$caps[] = $p;
}
}
}
}
return array_values(array_unique($caps));
}
}