<?php
/**
 * Slate — Mailer.
 *
 * Minimal email sender. Uses PHPMailer if available (vendor/),
 * falls back to PHP's mail() if not. SMTP credentials are read
 * from the `settings` table:
 *
 *   smtp_host, smtp_port, smtp_user, smtp_pass,
 *   smtp_from_email, smtp_from_name, smtp_encryption
 *
 * Plugins that need queueing, retry, logging, or templates wrap
 * this with their own infrastructure.
 */

class Mailer {

    /**
     * Send an email. Returns true on success, false on failure.
     *
     * $toName is optional — most callers don't have the recipient's display
     * name handy, and SMTP doesn't require one. Pass it when you have it
     * (e.g., when sending to a known customer record). The old signature
     * had $toName as the SECOND required argument and exactly one caller
     * (settings.php's test-email button) was calling it with three args
     * `(to, subject, body)`, which PHP mapped positionally as
     * `(to, toName, subject)` and then fatal'd on the missing $bodyHtml
     * — the source of the test-email 500.
     *
     * If $log is true (default), the attempt is recorded to the audit log.
     */
    /**
     * @param array<int,array{name:string,data?:string,path?:string,mime?:string}> $attachments
     *   Each item is either an in-memory file (`data` + `name` [+ `mime`]) or a
     *   disk file (`path` + optional `name`). Defaults to no attachments, so
     *   every existing caller keeps working unchanged.
     */
    public static function send(string $to, string $subject, string $bodyHtml, string $toName = '', bool $log = true, array $attachments = []): bool {
        $host = Database::setting('smtp_host');

        $ok = false;
        $err = '';

        if (class_exists('PHPMailer\\PHPMailer\\PHPMailer') && $host) {
            [$ok, $err] = self::sendViaPHPMailer($to, $toName, $subject, $bodyHtml, $attachments);
        } else {
            [$ok, $err] = self::sendViaMail($to, $toName, $subject, $bodyHtml, $attachments);
        }

        if ($log && class_exists('AuditLog')) {
            AuditLog::record('mail.send', $to, [
                'subject' => $subject,
                'ok'      => $ok,
                'error'   => $err,
            ]);
        }

        return $ok;
    }

    /** @return array{0:bool, 1:string} [success, errorMessage] */
    private static function sendViaPHPMailer(string $to, string $toName, string $subject, string $body, array $attachments = []): array {
        try {
            $mail = new \PHPMailer\PHPMailer\PHPMailer(true);
            $mail->isSMTP();
            $mail->Host = Database::setting('smtp_host');
            $mail->Port = (int)(Database::setting('smtp_port') ?: 587);

            $user = Database::setting('smtp_user');
            $pass = Database::setting('smtp_pass');
            if ($user) {
                $mail->SMTPAuth = true;
                $mail->Username = $user;
                $mail->Password = function_exists('slate_decrypt_secret')
                    ? (slate_decrypt_secret((string)$pass) ?? $pass)
                    : $pass;
            }

            $enc = Database::setting('smtp_encryption') ?: 'tls';
            if ($enc === 'ssl') {
                $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
            } elseif ($enc === 'tls' || $enc === 'starttls') {
                $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
            }

            $fromEmail = Database::setting('smtp_from_email') ?: 'no-reply@example.com';
            $fromName  = Database::setting('smtp_from_name')  ?: 'Slate';
            $mail->setFrom($fromEmail, $fromName);
            // Pass the recipient name only when we have one; PHPMailer
            // accepts an empty string but it produces an awkward
            // "<>name@host" header on some MTAs.
            if ($toName !== '') {
                $mail->addAddress($to, $toName);
            } else {
                $mail->addAddress($to);
            }
            $mail->isHTML(true);
            $mail->Subject = $subject;
            $mail->Body    = $body;
            $mail->AltBody = strip_tags($body);

            foreach ($attachments as $att) {
                $name = (string)($att['name'] ?? 'attachment');
                $mime = (string)($att['mime'] ?? 'application/octet-stream');
                if (isset($att['data'])) {
                    $mail->addStringAttachment((string)$att['data'], $name, \PHPMailer\PHPMailer\PHPMailer::ENCODING_BASE64, $mime);
                } elseif (!empty($att['path']) && is_file($att['path'])) {
                    $mail->addAttachment((string)$att['path'], $name);
                }
            }

            $mail->send();
            return [true, ''];
        } catch (\Throwable $e) {
            return [false, $e->getMessage()];
        }
    }

    /** @return array{0:bool, 1:string} */
    private static function sendViaMail(string $to, string $toName, string $subject, string $body, array $attachments = []): array {
        $clean = fn(string $s) => str_replace(["\r", "\n", "\0"], '', $s);
        $to       = $clean($to);
        $toName   = $clean($toName);
        $subject  = $clean($subject);
        $fromMail = $clean((string)(Database::setting('smtp_from_email') ?: 'no-reply@example.com'));
        $fromName = $clean((string)(Database::setting('smtp_from_name')  ?: 'Slate'));

        $headers  = "MIME-Version: 1.0\r\n";
        $headers .= "From: {$fromName} <{$fromMail}>\r\n";
        $headers .= "Reply-To: {$fromMail}\r\n";

        if (empty($attachments)) {
            $headers .= "Content-Type: text/html; charset=UTF-8\r\n";
            $ok = @mail($to, $subject, $body, $headers);
            return [(bool)$ok, $ok ? '' : 'mail() returned false'];
        }

        // multipart/mixed: HTML part + each attachment as base64.
        $boundary = 'slate_' . bin2hex(random_bytes(12));
        $headers .= "Content-Type: multipart/mixed; boundary=\"{$boundary}\"\r\n";

        $nl  = "\r\n";
        $msg  = "--{$boundary}{$nl}";
        $msg .= "Content-Type: text/html; charset=UTF-8{$nl}";
        $msg .= "Content-Transfer-Encoding: 8bit{$nl}{$nl}";
        $msg .= $body . $nl;
        foreach ($attachments as $att) {
            $name = $clean((string)($att['name'] ?? 'attachment'));
            $mime = (string)($att['mime'] ?? 'application/octet-stream');
            $raw  = isset($att['data']) ? (string)$att['data']
                  : (!empty($att['path']) && is_file($att['path']) ? (string)@file_get_contents($att['path']) : '');
            if ($raw === '') continue;
            $msg .= "--{$boundary}{$nl}";
            $msg .= "Content-Type: {$mime}; name=\"{$name}\"{$nl}";
            $msg .= "Content-Transfer-Encoding: base64{$nl}";
            $msg .= "Content-Disposition: attachment; filename=\"{$name}\"{$nl}{$nl}";
            $msg .= chunk_split(base64_encode($raw)) . $nl;
        }
        $msg .= "--{$boundary}--{$nl}";

        $ok = @mail($to, $subject, $msg, $headers);
        return [(bool)$ok, $ok ? '' : 'mail() returned false'];
    }
}
