CluPilotCloud/app/Mail/Transport/MailboxTransport.php

107 lines
3.7 KiB
PHP

<?php
namespace App\Mail\Transport;
use App\Services\Mail\MailboxResolver;
use App\Support\Settings;
use Illuminate\Mail\Transport\LogTransport;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\RawMessage;
/**
* Resolves its credentials when it sends, not when it is built.
*
* The mails are queued. A worker builds its mailer once and lives for hours, so
* anything decided in a constructor is decided for the rest of that worker's
* life — including a password the operator has since corrected in the console.
*
* The delegate is cached against a FINGERPRINT of the credentials rather than
* rebuilt per message: correct when they change, and no new SMTP connection per
* mail when they do not.
*/
class MailboxTransport implements TransportInterface
{
private ?TransportInterface $delegate = null;
private ?string $fingerprint = null;
public function __construct(private readonly string $purpose) {}
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
return $this->delegate()->send($message, $envelope);
}
public function __toString(): string
{
return 'mailbox://'.$this->purpose.'/'.($this->describe() ?? 'unconfigured');
}
/** What this transport currently points at — used by __toString and tests. */
private function describe(): ?string
{
if ($this->isLogging()) {
return 'log';
}
return app(MailboxResolver::class)->for($this->purpose)?->address;
}
/**
* Delivery is off wherever the default mailer is the log.
*
* This is what keeps a development machine from mailing real customers: the
* mailables name a purpose, so without this check they would bypass
* MAIL_MAILER=log entirely and send for real.
*/
private function isLogging(): bool
{
return config('mail.default') === 'log';
}
private function delegate(): TransportInterface
{
if ($this->isLogging()) {
return new LogTransport(Log::channel(config('mail.mailers.log.channel')));
}
$box = app(MailboxResolver::class)->for($this->purpose);
if ($box === null || ! $box->isConfigured()) {
// Loud, not silent: a mail with no mailbox behind it must not look
// like it was sent.
throw new \RuntimeException(
"No configured mailbox for mail purpose [{$this->purpose}]."
);
}
$host = (string) Settings::get('mail.host', '');
$port = (int) Settings::get('mail.port', 587);
$encryption = (string) Settings::get('mail.encryption', 'tls');
$fingerprint = md5(implode('|', [
$host, $port, $encryption, $box->smtpUsername(), (string) $box->password,
]));
if ($this->delegate === null || $this->fingerprint !== $fingerprint) {
// The third argument is IMPLICIT TLS (smtps, port 465) — not
// STARTTLS. EsmtpTransport negotiates STARTTLS by itself on a
// plain connection, which is what port 587 wants. Passing true for
// 'tls' here would open an SSL socket against a server expecting
// STARTTLS and hang.
$transport = new EsmtpTransport($host, $port, $encryption === 'ssl' || $port === 465);
$transport->setUsername($box->smtpUsername());
$transport->setPassword((string) $box->password);
$this->delegate = $transport;
$this->fingerprint = $fingerprint;
}
return $this->delegate;
}
}