update patch 1

This commit is contained in:
fiqhpratama
2024-12-02 01:18:34 +07:00
parent 453d9bb470
commit 25026b0a85
3380 changed files with 103529 additions and 363623 deletions
+11
View File
@@ -1,6 +1,17 @@
CHANGELOG
=========
7.2
---
* Deprecate `TransportFactoryTestCase`, extend `AbstractTransportFactoryTestCase` instead
The `testIncompleteDsnException()` test is no longer provided by default. If you make use of it by implementing the `incompleteDsnProvider()` data providers,
you now need to use the `IncompleteDsnTestTrait`.
* Make `TransportFactoryTestCase` compatible with PHPUnit 10+
* Support unicode email addresses such as "dømi@dømi.example"
7.1
---
+3 -4
View File
@@ -25,11 +25,10 @@ final class DelayedEnvelope extends Envelope
{
private bool $senderSet = false;
private bool $recipientsSet = false;
private Message $message;
public function __construct(Message $message)
{
$this->message = $message;
public function __construct(
private Message $message,
) {
}
public function setSender(Address $sender): void
+31 -2
View File
@@ -46,7 +46,7 @@ class Envelope
{
// to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers
if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) {
throw new InvalidArgumentException(sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress()));
throw new InvalidArgumentException(\sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress()));
}
$this->sender = $sender;
}
@@ -72,7 +72,7 @@ class Envelope
$this->recipients = [];
foreach ($recipients as $recipient) {
if (!$recipient instanceof Address) {
throw new InvalidArgumentException(sprintf('A recipient must be an instance of "%s" (got "%s").', Address::class, get_debug_type($recipient)));
throw new InvalidArgumentException(\sprintf('A recipient must be an instance of "%s" (got "%s").', Address::class, get_debug_type($recipient)));
}
$this->recipients[] = new Address($recipient->getAddress());
}
@@ -85,4 +85,33 @@ class Envelope
{
return $this->recipients;
}
/**
* Returns true if any address' localpart contains at least one
* non-ASCII character, and false if all addresses have all-ASCII
* localparts.
*
* This helps to decide whether to the SMTPUTF8 extensions (RFC
* 6530 and following) for any given message.
*
* The SMTPUTF8 extension is strictly required if any address
* contains a non-ASCII character in its localpart. If non-ASCII
* is only used in domains (e.g. horst@freiherr-von-mühlhausen.de)
* then it is possible to send the message using IDN encoding
* instead of SMTPUTF8. The most common software will display the
* message as intended.
*/
public function anyAddressHasUnicodeLocalpart(): bool
{
if ($this->getSender()->hasUnicodeLocalpart()) {
return true;
}
foreach ($this->getRecipients() as $r) {
if ($r->hasUnicodeLocalpart()) {
return true;
}
}
return false;
}
}
+8 -12
View File
@@ -24,21 +24,17 @@ use Symfony\Contracts\EventDispatcher\Event;
*/
final class MessageEvent extends Event
{
private RawMessage $message;
private Envelope $envelope;
private string $transport;
private bool $queued;
private bool $rejected = false;
/** @var StampInterface[] */
private array $stamps = [];
public function __construct(RawMessage $message, Envelope $envelope, string $transport, bool $queued = false)
{
$this->message = $message;
$this->envelope = $envelope;
$this->transport = $transport;
$this->queued = $queued;
public function __construct(
private RawMessage $message,
private Envelope $envelope,
private string $transport,
private bool $queued = false,
) {
}
public function getMessage(): RawMessage
@@ -85,7 +81,7 @@ final class MessageEvent extends Event
public function addStamp(StampInterface $stamp): void
{
if (!$this->queued) {
throw new LogicException(sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__));
throw new LogicException(\sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__));
}
$this->stamps[] = $stamp;
@@ -97,7 +93,7 @@ final class MessageEvent extends Event
public function getStamps(): array
{
if (!$this->queued) {
throw new LogicException(sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__));
throw new LogicException(\sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__));
}
return $this->stamps;
+7 -8
View File
@@ -39,14 +39,13 @@ class MessageListener implements EventSubscriberInterface
'bcc' => self::HEADER_ADD,
];
private ?Headers $headers;
private array $headerRules = [];
private ?BodyRendererInterface $renderer;
public function __construct(?Headers $headers = null, ?BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES)
{
$this->headers = $headers;
$this->renderer = $renderer;
public function __construct(
private ?Headers $headers = null,
private ?BodyRendererInterface $renderer = null,
array $headerRules = self::DEFAULT_RULES,
) {
foreach ($headerRules as $headerName => $rule) {
$this->addHeaderRule($headerName, $rule);
}
@@ -55,7 +54,7 @@ class MessageListener implements EventSubscriberInterface
public function addHeaderRule(string $headerName, int $rule): void
{
if ($rule < 1 || $rule > 3) {
throw new InvalidArgumentException(sprintf('The "%d" rule is not supported.', $rule));
throw new InvalidArgumentException(\sprintf('The "%d" rule is not supported.', $rule));
}
$this->headerRules[strtolower($headerName)] = $rule;
@@ -105,7 +104,7 @@ class MessageListener implements EventSubscriberInterface
$h = $headers->get($name);
if (!$h instanceof MailboxListHeader) {
throw new RuntimeException(sprintf('Unable to set header "%s".', $name));
throw new RuntimeException(\sprintf('Unable to set header "%s".', $name));
}
Headers::checkHeaderClass($header);
+6 -6
View File
@@ -18,13 +18,13 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
*/
class HttpTransportException extends TransportException
{
private ResponseInterface $response;
public function __construct(string $message, ResponseInterface $response, int $code = 0, ?\Throwable $previous = null)
{
public function __construct(
string $message,
private ResponseInterface $response,
int $code = 0,
?\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
$this->response = $response;
}
public function getResponse(): ResponseInterface
@@ -48,6 +48,10 @@ class UnsupportedSchemeException extends LogicException
'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class,
'package' => 'symfony/mailjet-mailer',
],
'mailomat' => [
'class' => Bridge\Mailomat\Transport\MailomatTransportFactory::class,
'package' => 'symfony/mailomat-mailer',
],
'mailpace' => [
'class' => Bridge\MailPace\Transport\MailPaceTransportFactory::class,
'package' => 'symfony/mail-pace-mailer',
@@ -56,10 +60,18 @@ class UnsupportedSchemeException extends LogicException
'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class,
'package' => 'symfony/mailchimp-mailer',
],
'postal' => [
'class' => Bridge\Postal\Transport\PostalTransportFactory::class,
'package' => 'symfony/postal-mailer',
],
'postmark' => [
'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class,
'package' => 'symfony/postmark-mailer',
],
'mailtrap' => [
'class' => Bridge\Mailtrap\Transport\MailtrapTransportFactory::class,
'package' => 'symfony/mailtrap-mailer',
],
'resend' => [
'class' => Bridge\Resend\Transport\ResendTransportFactory::class,
'package' => 'symfony/resend-mailer',
@@ -76,6 +88,10 @@ class UnsupportedSchemeException extends LogicException
'class' => Bridge\Amazon\Transport\SesTransportFactory::class,
'package' => 'symfony/amazon-mailer',
],
'sweego' => [
'class' => Bridge\Sweego\Transport\SweegoTransportFactory::class,
'package' => 'symfony/sweego-mailer',
],
];
public function __construct(Dsn $dsn, ?string $name = null, array $supported = [])
@@ -86,14 +102,14 @@ class UnsupportedSchemeException extends LogicException
}
$package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null;
if ($package && !class_exists($package['class'])) {
parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed. Try running "composer require %s".', $provider, $package['package']));
parent::__construct(\sprintf('Unable to send emails via "%s" as the bridge is not installed. Try running "composer require %s".', $provider, $package['package']));
return;
}
$message = sprintf('The "%s" scheme is not supported', $dsn->getScheme());
$message = \sprintf('The "%s" scheme is not supported', $dsn->getScheme());
if ($name && $supported) {
$message .= sprintf('; supported schemes for mailer "%s" are: "%s"', $name, implode('", "', $supported));
$message .= \sprintf('; supported schemes for mailer "%s" are: "%s"', $name, implode('", "', $supported));
}
parent::__construct($message.'.');
+4 -6
View File
@@ -18,12 +18,10 @@ use Symfony\Component\Mime\Header\UnstructuredHeader;
*/
final class MetadataHeader extends UnstructuredHeader
{
private string $key;
public function __construct(string $key, string $value)
{
$this->key = $key;
public function __construct(
private string $key,
string $value,
) {
parent::__construct('X-Metadata-'.$key, $value);
}
+5 -9
View File
@@ -25,15 +25,11 @@ use Symfony\Component\Mime\RawMessage;
*/
final class Mailer implements MailerInterface
{
private TransportInterface $transport;
private ?MessageBusInterface $bus;
private ?EventDispatcherInterface $dispatcher;
public function __construct(TransportInterface $transport, ?MessageBusInterface $bus = null, ?EventDispatcherInterface $dispatcher = null)
{
$this->transport = $transport;
$this->bus = $bus;
$this->dispatcher = $dispatcher;
public function __construct(
private TransportInterface $transport,
private ?MessageBusInterface $bus = null,
private ?EventDispatcherInterface $dispatcher = null,
) {
}
public function send(RawMessage $message, ?Envelope $envelope = null): void
+3 -5
View File
@@ -19,11 +19,9 @@ use Symfony\Component\Mailer\Transport\TransportInterface;
*/
class MessageHandler
{
private TransportInterface $transport;
public function __construct(TransportInterface $transport)
{
$this->transport = $transport;
public function __construct(
private TransportInterface $transport,
) {
}
public function __invoke(SendEmailMessage $message): ?SentMessage
+4 -7
View File
@@ -19,13 +19,10 @@ use Symfony\Component\Mime\RawMessage;
*/
class SendEmailMessage
{
private RawMessage $message;
private ?Envelope $envelope;
public function __construct(RawMessage $message, ?Envelope $envelope = null)
{
$this->message = $message;
$this->envelope = $envelope;
public function __construct(
private RawMessage $message,
private ?Envelope $envelope = null,
) {
}
public function getMessage(): RawMessage
+13
View File
@@ -64,6 +64,15 @@ $email = (new TemplatedEmail())
$mailer->send($email);
```
Sponsor
-------
The Mailer component for Symfony 7.2 is [backed][1] by:
* [Sweego][2], a European email and SMS sending platform for developers and product builders. Easily create, deliver, and monitor your emails and notifications.
Help Symfony by [sponsoring][3] its development!
Resources
---------
@@ -72,3 +81,7 @@ Resources
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[2]: https://www.sweego.io/
[3]: https://symfony.com/sponsor
+4 -4
View File
@@ -21,19 +21,19 @@ class SentMessage
{
private RawMessage $original;
private RawMessage $raw;
private Envelope $envelope;
private string $messageId;
private string $debug = '';
/**
* @internal
*/
public function __construct(RawMessage $message, Envelope $envelope)
{
public function __construct(
RawMessage $message,
private Envelope $envelope,
) {
$message->ensureValidity();
$this->original = $message;
$this->envelope = $envelope;
if ($message instanceof Message) {
$message = clone $message;
+7 -11
View File
@@ -16,20 +16,16 @@ use Symfony\Component\Mailer\Event\MessageEvents;
final class EmailCount extends Constraint
{
private int $expectedValue;
private ?string $transport;
private bool $queued;
public function __construct(int $expectedValue, ?string $transport = null, bool $queued = false)
{
$this->expectedValue = $expectedValue;
$this->transport = $transport;
$this->queued = $queued;
public function __construct(
private int $expectedValue,
private ?string $transport = null,
private bool $queued = false,
) {
}
public function toString(): string
{
return sprintf('%shas %s "%d" emails', $this->transport ? $this->transport.' ' : '', $this->queued ? 'queued' : 'sent', $this->expectedValue);
return \sprintf('%shas %s "%d" emails', $this->transport ? $this->transport.' ' : '', $this->queued ? 'queued' : 'sent', $this->expectedValue);
}
/**
@@ -45,7 +41,7 @@ final class EmailCount extends Constraint
*/
protected function failureDescription($events): string
{
return sprintf('the Transport %s (%d %s)', $this->toString(), $this->countEmails($events), $this->queued ? 'queued' : 'sent');
return \sprintf('the Transport %s (%d %s)', $this->toString(), $this->countEmails($events), $this->queued ? 'queued' : 'sent');
}
private function countEmails(MessageEvents $events): int
+10 -63
View File
@@ -11,13 +11,8 @@
namespace Symfony\Component\Mailer\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\IncompleteDsnException;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -25,81 +20,33 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
* A test case to ease testing Transport Factory.
*
* @author Konstantin Myakshin <molodchick@gmail.com>
*
* @deprecated since Symfony 7.2, use AbstractTransportFactoryTestCase instead
*/
abstract class TransportFactoryTestCase extends TestCase
abstract class TransportFactoryTestCase extends AbstractTransportFactoryTestCase
{
protected const USER = 'u$er';
protected const PASSWORD = 'pa$s';
use IncompleteDsnTestTrait;
protected EventDispatcherInterface $dispatcher;
protected HttpClientInterface $client;
protected LoggerInterface $logger;
abstract public function getFactory(): TransportFactoryInterface;
abstract public static function supportsProvider(): iterable;
abstract public static function createProvider(): iterable;
/**
* @psalm-return iterable<array{0: Dsn, 1?: string|null}>
*/
public static function unsupportedSchemeProvider(): iterable
{
return [];
}
/**
* @psalm-return iterable<array{0: Dsn}>
*/
public static function incompleteDsnProvider(): iterable
{
return [];
}
/**
* @dataProvider supportsProvider
*/
public function testSupports(Dsn $dsn, bool $supports)
{
$factory = $this->getFactory();
$this->assertSame($supports, $factory->supports($dsn));
}
/**
* @dataProvider createProvider
*/
public function testCreate(Dsn $dsn, TransportInterface $transport)
{
$factory = $this->getFactory();
$this->assertEquals($transport, $factory->create($dsn));
if (str_contains('smtp', $dsn->getScheme())) {
$this->assertStringMatchesFormat($dsn->getScheme().'://%S'.$dsn->getHost().'%S', (string) $transport);
}
}
/**
* @dataProvider unsupportedSchemeProvider
*/
public function testUnsupportedSchemeException(Dsn $dsn, ?string $message = null)
{
$factory = $this->getFactory();
$this->expectException(UnsupportedSchemeException::class);
if (null !== $message) {
$this->expectExceptionMessage($message);
}
$factory->create($dsn);
}
/**
* @dataProvider incompleteDsnProvider
*/
public function testIncompleteDsnException(Dsn $dsn)
{
$factory = $this->getFactory();
$this->expectException(IncompleteDsnException::class);
$factory->create($dsn);
}
protected function getDispatcher(): EventDispatcherInterface
{
return $this->dispatcher ??= $this->createMock(EventDispatcherInterface::class);
+12 -6
View File
@@ -22,11 +22,15 @@ use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailomat\Transport\MailomatTransportFactory;
use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory;
use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalTransportFactory;
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory;
use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory;
use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory;
use Symfony\Component\Mailer\Bridge\Sweego\Transport\SweegoTransportFactory;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\Dsn;
@@ -55,17 +59,19 @@ final class Transport
MailerSendTransportFactory::class,
MailgunTransportFactory::class,
MailjetTransportFactory::class,
MailomatTransportFactory::class,
MailPaceTransportFactory::class,
MandrillTransportFactory::class,
PostalTransportFactory::class,
PostmarkTransportFactory::class,
MailtrapTransportFactory::class,
ResendTransportFactory::class,
ScalewayTransportFactory::class,
SendgridTransportFactory::class,
SesTransportFactory::class,
SweegoTransportFactory::class,
];
private iterable $factories;
public static function fromDsn(#[\SensitiveParameter] string $dsn, ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): TransportInterface
{
$factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger)));
@@ -83,9 +89,9 @@ final class Transport
/**
* @param TransportFactoryInterface[] $factories
*/
public function __construct(iterable $factories)
{
$this->factories = $factories;
public function __construct(
private iterable $factories,
) {
}
public function fromStrings(#[\SensitiveParameter] array $dsns): Transports
@@ -144,7 +150,7 @@ final class Transport
}
if (preg_match('{(\w+)\(}A', $dsn, $matches, 0, $offset)) {
throw new InvalidArgumentException(sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords))));
throw new InvalidArgumentException(\sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords))));
}
if ($pos = strcspn($dsn, ' )', $offset)) {
+1 -1
View File
@@ -31,7 +31,7 @@ abstract class AbstractApiTransport extends AbstractHttpTransport
try {
$email = MessageConverter::toEmail($message->getOriginalMessage());
} catch (\Exception $e) {
throw new RuntimeException(sprintf('Unable to send message with the "%s" transport: ', __CLASS__).$e->getMessage(), 0, $e);
throw new RuntimeException(\sprintf('Unable to send message with the "%s" transport: ', __CLASS__).$e->getMessage(), 0, $e);
}
return $this->doSendApi($message, $email, $message->getEnvelope());
+6 -5
View File
@@ -26,14 +26,15 @@ abstract class AbstractHttpTransport extends AbstractTransport
{
protected ?string $host = null;
protected ?int $port = null;
protected ?HttpClientInterface $client;
public function __construct(?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
{
$this->client = $client;
public function __construct(
protected ?HttpClientInterface $client = null,
?EventDispatcherInterface $dispatcher = null,
?LoggerInterface $logger = null,
) {
if (null === $client) {
if (!class_exists(HttpClient::class)) {
throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
throw new \LogicException(\sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
}
$this->client = HttpClient::create();
+6 -6
View File
@@ -30,14 +30,14 @@ use Symfony\Component\Mime\RawMessage;
*/
abstract class AbstractTransport implements TransportInterface
{
private ?EventDispatcherInterface $dispatcher;
private LoggerInterface $logger;
private float $rate = 0;
private float $lastSent = 0;
public function __construct(?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
{
$this->dispatcher = $dispatcher;
public function __construct(
private ?EventDispatcherInterface $dispatcher = null,
?LoggerInterface $logger = null,
) {
$this->logger = $logger ?? new NullLogger();
}
@@ -81,7 +81,7 @@ abstract class AbstractTransport implements TransportInterface
$message = $event->getMessage();
if ($message instanceof TemplatedEmail && !$message->isRendered()) {
throw new LogicException(sprintf('You must configure a "%s" when a "%s" instance has a text or HTML template set.', BodyRendererInterface::class, get_debug_type($message)));
throw new LogicException(\sprintf('You must configure a "%s" when a "%s" instance has a text or HTML template set.', BodyRendererInterface::class, get_debug_type($message)));
}
$sentMessage = new SentMessage($message, $envelope);
@@ -128,7 +128,7 @@ abstract class AbstractTransport implements TransportInterface
$sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent);
if (0 < $sleep) {
$this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep));
$this->logger->debug(\sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep));
usleep((int) ($sleep * 1000000));
}
$this->lastSent = microtime(true);
@@ -21,15 +21,11 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
abstract class AbstractTransportFactory implements TransportFactoryInterface
{
protected ?EventDispatcherInterface $dispatcher;
protected ?HttpClientInterface $client;
protected ?LoggerInterface $logger;
public function __construct(?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null)
{
$this->dispatcher = $dispatcher;
$this->client = $client;
$this->logger = $logger;
public function __construct(
protected ?EventDispatcherInterface $dispatcher = null,
protected ?HttpClientInterface $client = null,
protected ?LoggerInterface $logger = null,
) {
}
public function supports(Dsn $dsn): bool
+8 -15
View File
@@ -18,21 +18,14 @@ use Symfony\Component\Mailer\Exception\InvalidArgumentException;
*/
final class Dsn
{
private string $scheme;
private string $host;
private ?string $user;
private ?string $password;
private ?int $port;
private array $options;
public function __construct(string $scheme, string $host, ?string $user = null, #[\SensitiveParameter] ?string $password = null, ?int $port = null, array $options = [])
{
$this->scheme = $scheme;
$this->host = $host;
$this->user = $user;
$this->password = $password;
$this->port = $port;
$this->options = $options;
public function __construct(
private string $scheme,
private string $host,
private ?string $user = null,
#[\SensitiveParameter] private ?string $password = null,
private ?int $port = null,
private array $options = [],
) {
}
public static function fromString(#[\SensitiveParameter] string $dsn): self
+6 -8
View File
@@ -28,22 +28,20 @@ class RoundRobinTransport implements TransportInterface
* @var \SplObjectStorage<TransportInterface, float>
*/
private \SplObjectStorage $deadTransports;
private array $transports = [];
private int $retryPeriod;
private int $cursor = -1;
/**
* @param TransportInterface[] $transports
*/
public function __construct(array $transports, int $retryPeriod = 60)
{
public function __construct(
private array $transports,
private int $retryPeriod = 60,
) {
if (!$transports) {
throw new TransportException(sprintf('"%s" must have at least one transport configured.', static::class));
throw new TransportException(\sprintf('"%s" must have at least one transport configured.', static::class));
}
$this->transports = $transports;
$this->deadTransports = new \SplObjectStorage();
$this->retryPeriod = $retryPeriod;
}
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
@@ -55,7 +53,7 @@ class RoundRobinTransport implements TransportInterface
return $transport->send($message, $envelope);
} catch (TransportExceptionInterface $e) {
$exception ??= new TransportException('All transports failed.');
$exception->appendDebug(sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
$exception->appendDebug(\sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
$this->deadTransports[$transport] = microtime(true);
}
}
+3 -3
View File
@@ -55,7 +55,7 @@ class SendmailTransport extends AbstractTransport
if (null !== $command) {
if (!str_contains($command, ' -bs') && !str_contains($command, ' -t')) {
throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command));
throw new \InvalidArgumentException(\sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command));
}
$this->command = $command;
@@ -89,7 +89,7 @@ class SendmailTransport extends AbstractTransport
protected function doSend(SentMessage $message): void
{
$this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
$this->getLogger()->debug(\sprintf('Email transport "%s" starting', __CLASS__));
$command = $this->command;
@@ -119,6 +119,6 @@ class SendmailTransport extends AbstractTransport
$this->stream->flush();
$this->stream->terminate();
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
$this->getLogger()->debug(\sprintf('Email transport "%s" stopped', __CLASS__));
}
}
@@ -34,7 +34,7 @@ class CramMd5Authenticator implements AuthenticatorInterface
$challenge = $client->executeCommand("AUTH CRAM-MD5\r\n", [334]);
$challenge = base64_decode(substr($challenge, 4));
$message = base64_encode($client->getUsername().' '.$this->getResponse($client->getPassword(), $challenge));
$client->executeCommand(sprintf("%s\r\n", $message), [235]);
$client->executeCommand(\sprintf("%s\r\n", $message), [235]);
}
/**
@@ -58,8 +58,7 @@ class CramMd5Authenticator implements AuthenticatorInterface
$kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64);
$inner = pack('H32', md5($kipad.$challenge));
$digest = md5($kopad.$inner);
return $digest;
return md5($kopad.$inner);
}
}
@@ -31,7 +31,7 @@ class LoginAuthenticator implements AuthenticatorInterface
public function authenticate(EsmtpTransport $client): void
{
$client->executeCommand("AUTH LOGIN\r\n", [334]);
$client->executeCommand(sprintf("%s\r\n", base64_encode($client->getUsername())), [334]);
$client->executeCommand(sprintf("%s\r\n", base64_encode($client->getPassword())), [235]);
$client->executeCommand(\sprintf("%s\r\n", base64_encode($client->getUsername())), [334]);
$client->executeCommand(\sprintf("%s\r\n", base64_encode($client->getPassword())), [235]);
}
}
@@ -30,6 +30,6 @@ class PlainAuthenticator implements AuthenticatorInterface
*/
public function authenticate(EsmtpTransport $client): void
{
$client->executeCommand(sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]);
$client->executeCommand(\sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]);
}
}
+11 -6
View File
@@ -142,10 +142,10 @@ class EsmtpTransport extends SmtpTransport
private function doEhloCommand(): string
{
try {
$response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
$response = $this->executeCommand(\sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
} catch (TransportExceptionInterface $e) {
try {
return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
return parent::executeCommand(\sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
} catch (TransportExceptionInterface $ex) {
if (!$ex->getCode()) {
throw $e;
@@ -169,7 +169,7 @@ class EsmtpTransport extends SmtpTransport
throw new TransportException('Unable to connect with STARTTLS.');
}
$response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
$response = $this->executeCommand(\sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
$this->capabilities = $this->parseCapabilities($response);
}
@@ -195,6 +195,11 @@ class EsmtpTransport extends SmtpTransport
return $capabilities;
}
protected function serverSupportsSmtpUtf8(): bool
{
return \array_key_exists('SMTPUTF8', $this->capabilities);
}
private function handleAuth(array $modes): void
{
if (!$this->username) {
@@ -231,12 +236,12 @@ class EsmtpTransport extends SmtpTransport
}
if (!$authNames) {
throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes)), $code ?: 504);
throw new TransportException(\sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes)), $code ?: 504);
}
$message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames));
$message = \sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames));
foreach ($errors as $name => $error) {
$message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error);
$message .= \sprintf(' Authenticator "%s" returned "%s".', $name, $error);
}
throw new TransportException($message, $code ?: 535);
@@ -11,6 +11,7 @@
namespace Symfony\Component\Mailer\Transport\Smtp;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
@@ -23,6 +24,10 @@ final class EsmtpTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) {
throw new UnsupportedSchemeException($dsn, 'smtp', $this->getSupportedSchemes());
}
$autoTls = '' === $dsn->getOption('auto_tls') || filter_var($dsn->getOption('auto_tls', true), \FILTER_VALIDATE_BOOL);
$tls = 'smtps' === $dsn->getScheme() ? true : ($autoTls ? null : false);
$port = $dsn->getPort(0);
+25 -16
View File
@@ -14,6 +14,7 @@ namespace Symfony\Component\Mailer\Transport\Smtp;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
@@ -171,7 +172,7 @@ class SmtpTransport extends AbstractTransport
public function __toString(): string
{
if ($this->stream instanceof SocketStream) {
$name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost());
$name = \sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost());
$port = $this->stream->getPort();
if (!(25 === $port || ($tls && 465 === $port))) {
$name .= ':'.$port;
@@ -211,7 +212,7 @@ class SmtpTransport extends AbstractTransport
try {
$envelope = $message->getEnvelope();
$this->doMailFromCommand($envelope->getSender()->getEncodedAddress());
$this->doMailFromCommand($envelope->getSender()->getEncodedAddress(), $envelope->anyAddressHasUnicodeLocalpart());
foreach ($envelope->getRecipients() as $recipient) {
$this->doRcptToCommand($recipient->getEncodedAddress());
}
@@ -227,7 +228,7 @@ class SmtpTransport extends AbstractTransport
} catch (\Exception $e) {
$this->stream->terminate();
$this->started = false;
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
$this->getLogger()->debug(\sprintf('Email transport "%s" stopped', __CLASS__));
throw $e;
}
$mtaResult = $this->executeCommand("\r\n.\r\n", [250]);
@@ -244,19 +245,27 @@ class SmtpTransport extends AbstractTransport
}
}
private function doHeloCommand(): void
protected function serverSupportsSmtpUtf8(): bool
{
$this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]);
return false;
}
private function doMailFromCommand(string $address): void
private function doHeloCommand(): void
{
$this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]);
$this->executeCommand(\sprintf("HELO %s\r\n", $this->domain), [250]);
}
private function doMailFromCommand(string $address, bool $smtputf8): void
{
if ($smtputf8 && !$this->serverSupportsSmtpUtf8()) {
throw new InvalidArgumentException('Invalid addresses: non-ASCII characters not supported in local-part of email.');
}
$this->executeCommand(\sprintf("MAIL FROM:<%s>%s\r\n", $address, $smtputf8 ? ' SMTPUTF8' : ''), [250]);
}
private function doRcptToCommand(string $address): void
{
$this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]);
$this->executeCommand(\sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]);
}
public function start(): void
@@ -265,7 +274,7 @@ class SmtpTransport extends AbstractTransport
return;
}
$this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
$this->getLogger()->debug(\sprintf('Email transport "%s" starting', __CLASS__));
$this->stream->initialize();
$this->assertResponseCode($this->getFullResponse(), [220]);
@@ -273,7 +282,7 @@ class SmtpTransport extends AbstractTransport
$this->started = true;
$this->lastMessageTime = 0;
$this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__));
$this->getLogger()->debug(\sprintf('Email transport "%s" started', __CLASS__));
}
/**
@@ -289,7 +298,7 @@ class SmtpTransport extends AbstractTransport
return;
}
$this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__));
$this->getLogger()->debug(\sprintf('Email transport "%s" stopping', __CLASS__));
try {
$this->executeCommand("QUIT\r\n", [221]);
@@ -297,7 +306,7 @@ class SmtpTransport extends AbstractTransport
} finally {
$this->stream->terminate();
$this->started = false;
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
$this->getLogger()->debug(\sprintf('Email transport "%s" stopped', __CLASS__));
}
}
@@ -327,10 +336,10 @@ class SmtpTransport extends AbstractTransport
$valid = \in_array($code, $codes);
if (!$valid || !$response) {
$codeStr = $code ? sprintf('code "%s"', $code) : 'empty code';
$responseStr = $response ? sprintf(', with message "%s"', trim($response)) : '';
$codeStr = $code ? \sprintf('code "%s"', $code) : 'empty code';
$responseStr = $response ? \sprintf(', with message "%s"', trim($response)) : '';
throw new UnexpectedResponseException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0);
throw new UnexpectedResponseException(\sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0);
}
}
@@ -359,7 +368,7 @@ class SmtpTransport extends AbstractTransport
$this->stop();
if (0 < $sleep = $this->restartThresholdSleep) {
$this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));
$this->getLogger()->debug(\sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));
sleep($sleep);
}
@@ -37,9 +37,9 @@ abstract class AbstractStream
public function write(string $bytes, bool $debug = true): void
{
if ($debug) {
$timestamp = date('c');
$timestamp = (new \DateTimeImmutable())->format('Y-m-d\TH:i:s.up');
foreach (explode("\n", trim($bytes)) as $line) {
$this->debug .= sprintf("[%s] > %s\n", $timestamp, $line);
$this->debug .= \sprintf("[%s] > %s\n", $timestamp, $line);
}
}
@@ -83,17 +83,17 @@ abstract class AbstractStream
if ('' === $line || false === $line) {
$metas = stream_get_meta_data($this->out);
if ($metas['timed_out']) {
throw new TransportException(sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription()));
throw new TransportException(\sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription()));
}
if ($metas['eof']) {
throw new TransportException(sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription()));
throw new TransportException(\sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription()));
}
if (false === $line) {
throw new TransportException(sprintf('Unable to read from connection to "%s": ', $this->getReadConnectionDescription()).error_get_last()['message']);
throw new TransportException(\sprintf('Unable to read from connection to "%s": ', $this->getReadConnectionDescription()).error_get_last()['message']);
}
}
$this->debug .= sprintf('[%s] < %s', date('c'), $line);
$this->debug .= \sprintf('[%s] < %s', (new \DateTimeImmutable())->format('Y-m-d\TH:i:s.up'), $line);
return $line;
}
@@ -151,7 +151,7 @@ final class SocketStream extends AbstractStream
$timeout = $this->getTimeout();
set_error_handler(function ($type, $msg) {
throw new TransportException(sprintf('Connection could not be established with host "%s": ', $this->url).$msg);
throw new TransportException(\sprintf('Connection could not be established with host "%s": ', $this->url).$msg);
});
try {
$this->stream = stream_socket_client($this->url, $errno, $errstr, $timeout, \STREAM_CLIENT_CONNECT, $streamContext);
+2 -2
View File
@@ -40,7 +40,7 @@ final class Transports implements TransportInterface
}
if (!$this->transports) {
throw new LogicException(sprintf('"%s" must have at least one transport configured.', __CLASS__));
throw new LogicException(\sprintf('"%s" must have at least one transport configured.', __CLASS__));
}
}
@@ -56,7 +56,7 @@ final class Transports implements TransportInterface
$headers->remove('X-Transport');
if (!isset($this->transports[$transport])) {
throw new InvalidArgumentException(sprintf('The "%s" transport does not exist (available transports: "%s").', $transport, implode('", "', array_keys($this->transports))));
throw new InvalidArgumentException(\sprintf('The "%s" transport does not exist (available transports: "%s").', $transport, implode('", "', array_keys($this->transports))));
}
try {
+1 -1
View File
@@ -21,7 +21,7 @@
"psr/event-dispatcher": "^1",
"psr/log": "^1|^2|^3",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/mime": "^7.2",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {