CluPilotCloud/app/Services/Mail/MailTlsPolicy.php

79 lines
3.5 KiB
PHP

<?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;
}
}