<?php

namespace App\Services\Mail;

use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;

/**
 * What a stored `mail.encryption` choice ("ssl", "tls", "none") actually
 * means on the wire — decided in exactly ONE place so MailboxTransport (real
 * sends) and MailboxTester (the test-send button) cannot silently enforce two
 * different policies for the same setting. Before this existed, both files
 * already independently re-derived the "ssl, or port 465 ⇒ implicit TLS"
 * half of this decision; a second dimension (require STARTTLS, or forbid it)
 * is exactly the kind of thing that drifts between two copies instead of
 * failing loudly when it does.
 *
 * EsmtpTransport (vendor/symfony/mailer/Transport/Smtp/EsmtpTransport.php in
 * the installed version) exposes three independent switches, not one:
 *
 * - The constructor's $tls argument: implicit TLS. true wraps the socket in
 *   TLS before speaking SMTP at all (smtps, port 465 in practice); false
 *   starts the connection in the clear, which is what STARTTLS needs to have
 *   something to upgrade FROM. Passing true against a STARTTLS-only server
 *   opens an SSL socket the server never answers; passing false against an
 *   implicit-TLS-only server sends a plaintext EHLO it will not understand.
 * - autoTls (default true): opportunistic STARTTLS. If the connection did
 *   not start under implicit TLS and the server's EHLO advertises STARTTLS,
 *   Symfony upgrades automatically — REGARDLESS of the stored encryption
 *   choice. A stored choice of "none" left at this default would still
 *   silently upgrade the moment a STARTTLS-capable server showed up.
 * - requireTls (default false): after negotiation, if TLS was not
 *   established by ANY means (implicit or STARTTLS), throw instead of
 *   continuing. This is what makes "tls" a requirement rather than a
 *   suggestion — a server (or a network attacker) that omits STARTTLS must
 *   fail loud, not send mailbox credentials in the clear.
 */
final class MailTlsPolicy
{
    private function __construct(
        public readonly bool $implicit,
        public readonly bool $autoTls,
        public readonly bool $requireTls,
    ) {}

    /**
     * Port 465 forces implicit TLS no matter what the stored encryption
     * setting says — unchanged from before this fix. A server listening
     * there speaks TLS from the very first byte and will not understand a
     * plaintext EHLO, so a mismatched setting must not be allowed to turn
     * into a hang.
     */
    public static function for(string $encryption, int $port): self
    {
        if ($encryption === 'ssl' || $port === 465) {
            return new self(implicit: true, autoTls: true, requireTls: true);
        }

        if ($encryption === 'tls') {
            return new self(implicit: false, autoTls: true, requireTls: true);
        }

        // 'none', and anything blank or unrecognised (a row predating the
        // in:tls,ssl,none validation, say): no implicit wrap, and no silent
        // upgrade either. Safe rather than strict — nobody explicitly chose
        // "tls" here, so failing loud for a value that merely isn't "ssl"
        // would be punishing a row this fix did not touch.
        return new self(implicit: false, autoTls: false, requireTls: false);
    }

    /** Applies the auto/require dimension to an already-built transport. */
    public function apply(EsmtpTransport $transport): EsmtpTransport
    {
        $transport->setAutoTls($this->autoTls);
        $transport->setRequireTls($this->requireTls);

        return $transport;
    }
}
