<?php
namespace PHPMailer\PHPMailer;

class PHPMailer {
    public const ENCRYPTION_SMTPS = 'ssl';
    public const ENCRYPTION_STARTTLS = 'tls';

    public string $Host = 'localhost';
    public bool $SMTPAuth = false;
    public string $Username = '';
    public string $Password = '';
    public string $SMTPSecure = '';
    public int $Port = 25;
    public string $CharSet = 'UTF-8';
    public string $Subject = '';
    public string $Body = '';
    public string $AltBody = '';
    public int $Timeout = 30;
    public string $ErrorInfo = '';

    private bool $useSmtp = false;
    private bool $html = false;
    private array $from = [];
    private array $to = [];

    public function __construct(bool $exceptions = false) {}

    public function isSMTP(): void { $this->useSmtp = true; }
    public function isHTML(bool $isHtml = true): void { $this->html = $isHtml; }
    public function setFrom(string $address, string $name = ''): void { $this->from = [$address, $name]; }
    public function addAddress(string $address, string $name = ''): void { $this->to[] = [$address, $name]; }

    public function send(): bool {
        if (empty($this->to)) {
            $this->ErrorInfo = 'No recipients configured';
            throw new Exception($this->ErrorInfo);
        }
        if ($this->useSmtp && $this->Host) {
            return $this->sendSmtp();
        }
        return $this->sendMailFunction();
    }

    private function sendMailFunction(): bool {
        $fromEmail = $this->from[0] ?? 'noreply@localhost';
        $headers = [
            'MIME-Version: 1.0',
            'Content-Type: ' . ($this->html ? 'text/html' : 'text/plain') . '; charset=' . $this->CharSet,
            'From: ' . $this->formatAddress($this->from),
        ];
        $ok = true;
        foreach ($this->to as $recipient) {
            $ok = mail($this->formatAddress($recipient), $this->Subject, $this->Body, implode("\r\n", $headers), '-f' . $fromEmail) && $ok;
        }
        if (!$ok) {
            $this->ErrorInfo = 'PHP mail() failed';
            throw new Exception($this->ErrorInfo);
        }
        return true;
    }

    private function sendSmtp(): bool {
        $host = $this->Host;
        $port = $this->Port ?: ($this->SMTPSecure === self::ENCRYPTION_SMTPS ? 465 : 25);
        $target = $this->SMTPSecure === self::ENCRYPTION_SMTPS ? 'ssl://' . $host : $host;
        $errno = 0;
        $errstr = '';
        $socket = @stream_socket_client($target . ':' . $port, $errno, $errstr, $this->Timeout, STREAM_CLIENT_CONNECT);
        if (!$socket) {
            $this->ErrorInfo = 'SMTP connect failed: ' . $errstr;
            throw new Exception($this->ErrorInfo);
        }
        stream_set_timeout($socket, $this->Timeout);
        $this->expect($socket, [220]);
        $hostname = $_SERVER['SERVER_NAME'] ?? 'localhost';
        $this->command($socket, 'EHLO ' . $hostname, [250]);
        if ($this->SMTPSecure === self::ENCRYPTION_STARTTLS) {
            $this->command($socket, 'STARTTLS', [220]);
            if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                throw new Exception('SMTP STARTTLS failed');
            }
            $this->command($socket, 'EHLO ' . $hostname, [250]);
        }
        if ($this->SMTPAuth) {
            $this->command($socket, 'AUTH LOGIN', [334]);
            $this->command($socket, base64_encode($this->Username), [334]);
            $this->command($socket, base64_encode($this->Password), [235]);
        }
        $fromEmail = $this->from[0] ?? 'noreply@localhost';
        $this->command($socket, 'MAIL FROM:<' . $fromEmail . '>', [250]);
        foreach ($this->to as $recipient) {
            $this->command($socket, 'RCPT TO:<' . $recipient[0] . '>', [250, 251]);
        }
        $this->command($socket, 'DATA', [354]);
        $message = $this->buildMessage();
        fwrite($socket, $message . "\r\n.\r\n");
        $this->expect($socket, [250]);
        $this->command($socket, 'QUIT', [221]);
        fclose($socket);
        return true;
    }

    private function buildMessage(): string {
        $headers = [
            'Date: ' . date('r'),
            'From: ' . $this->formatAddress($this->from),
            'To: ' . implode(', ', array_map(fn($to) => $this->formatAddress($to), $this->to)),
            'Subject: ' . $this->encodeHeader($this->Subject),
            'MIME-Version: 1.0',
            'Content-Type: ' . ($this->html ? 'text/html' : 'text/plain') . '; charset=' . $this->CharSet,
            'Content-Transfer-Encoding: 8bit',
        ];
        return implode("\r\n", $headers) . "\r\n\r\n" . str_replace("\n", "\r\n", $this->Body);
    }

    private function command($socket, string $command, array $codes): string {
        fwrite($socket, $command . "\r\n");
        return $this->expect($socket, $codes);
    }

    private function expect($socket, array $codes): string {
        $response = '';
        while (($line = fgets($socket, 515)) !== false) {
            $response .= $line;
            if (preg_match('/^([0-9]{3})\s/', $line, $match)) {
                $code = (int)$match[1];
                if (!in_array($code, $codes, true)) {
                    $this->ErrorInfo = trim($response);
                    throw new Exception('SMTP error: ' . trim($response));
                }
                return $response;
            }
        }
        throw new Exception('SMTP server did not respond');
    }

    private function formatAddress(array $address): string {
        $email = $address[0] ?? 'noreply@localhost';
        $name = $address[1] ?? '';
        return $name ? sprintf('"%s" <%s>', addcslashes($name, '"'), $email) : $email;
    }

    private function encodeHeader(string $value): string {
        return str_replace(["\r", "\n"], '', $value);
    }
}
