79 lines
3.5 KiB
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;
|
|
}
|
|
}
|