File manager - Edit - /home/autoph/public_html/projects/Rating-AutoHub/public/css/mailer.tar
Back
Mailer.php 0000644 00000005151 15025057122 0006467 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Messenger\SendEmailMessage; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ final class Mailer implements MailerInterface { private $transport; private $bus; private $dispatcher; public function __construct(TransportInterface $transport, MessageBusInterface $bus = null, EventDispatcherInterface $dispatcher = null) { $this->transport = $transport; $this->bus = $bus; $this->dispatcher = $dispatcher; } public function send(RawMessage $message, Envelope $envelope = null): void { if (null === $this->bus) { $this->transport->send($message, $envelope); return; } if (null !== $this->dispatcher) { // The dispatched event here has `queued` set to `true`; the goal is NOT to render the message, but to let // listeners do something before a message is sent to the queue. // We are using a cloned message as we still want to dispatch the **original** message, not the one modified by listeners. // That's because the listeners will run again when the email is sent via Messenger by the transport (see `AbstractTransport`). // Listeners should act depending on the `$queued` argument of the `MessageEvent` instance. $clonedMessage = clone $message; $clonedEnvelope = null !== $envelope ? clone $envelope : Envelope::create($clonedMessage); $event = new MessageEvent($clonedMessage, $clonedEnvelope, (string) $this->transport, true); $this->dispatcher->dispatch($event); } try { $this->bus->dispatch(new SendEmailMessage($message, $envelope)); } catch (HandlerFailedException $e) { foreach ($e->getNestedExceptions() as $nested) { if ($nested instanceof TransportExceptionInterface) { throw $nested; } } throw $e; } } } Transport/AbstractApiTransport.php 0000644 00000003061 15025057122 0013362 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\RuntimeException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Component\Mime\MessageConverter; use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ abstract class AbstractApiTransport extends AbstractHttpTransport { abstract protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface; protected function doSendHttp(SentMessage $message): ResponseInterface { 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); } return $this->doSendApi($message, $email, $message->getEnvelope()); } protected function getRecipients(Email $email, Envelope $envelope): array { return array_filter($envelope->getRecipients(), function (Address $address) use ($email) { return false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true); }); } } Transport/AbstractTransportFactory.php 0000644 00000003137 15025057122 0014264 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\IncompleteDsnException; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ abstract class AbstractTransportFactory implements TransportFactoryInterface { protected $dispatcher; protected $client; protected $logger; public function __construct(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null) { $this->dispatcher = $dispatcher; $this->client = $client; $this->logger = $logger; } public function supports(Dsn $dsn): bool { return \in_array($dsn->getScheme(), $this->getSupportedSchemes()); } abstract protected function getSupportedSchemes(): array; protected function getUser(Dsn $dsn): string { $user = $dsn->getUser(); if (null === $user) { throw new IncompleteDsnException('User is not set.'); } return $user; } protected function getPassword(Dsn $dsn): string { $password = $dsn->getPassword(); if (null === $password) { throw new IncompleteDsnException('Password is not set.'); } return $password; } } Transport/NullTransport.php 0000644 00000001216 15025057122 0012077 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\SentMessage; /** * Pretends messages have been sent, but just ignores them. * * @author Fabien Potencier <fabien@symfony.com> */ final class NullTransport extends AbstractTransport { protected function doSend(SentMessage $message): void { } public function __toString(): string { return 'null://'; } } Transport/NullTransportFactory.php 0000644 00000001535 15025057122 0013433 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ final class NullTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { if ('null' === $dsn->getScheme()) { return new NullTransport($this->dispatcher, $this->logger); } throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); } protected function getSupportedSchemes(): array { return ['null']; } } Transport/SendmailTransport.php 0000644 00000010260 15025057122 0012720 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream; use Symfony\Component\Mime\RawMessage; /** * SendmailTransport for sending mail through a Sendmail/Postfix (etc..) binary. * * Transport can be instantiated through SendmailTransportFactory or NativeTransportFactory: * * - SendmailTransportFactory to use most common sendmail path and recommended options * - NativeTransportFactory when configuration is set via php.ini * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn */ class SendmailTransport extends AbstractTransport { private string $command = '/usr/sbin/sendmail -bs'; private $stream; private $transport = null; /** * Constructor. * * Supported modes are -bs and -t, with any additional flags desired. * * The recommended mode is "-bs" since it is interactive and failure notifications are hence possible. * Note that the -t mode does not support error reporting and does not support Bcc properly (the Bcc headers are not removed). * * If using -t mode, you are strongly advised to include -oi or -i in the flags (like /usr/sbin/sendmail -oi -t) * * -f<sender> flag will be appended automatically if one is not present. */ public function __construct(string $command = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { parent::__construct($dispatcher, $logger); 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)); } $this->command = $command; } $this->stream = new ProcessStream(); if (str_contains($this->command, ' -bs')) { $this->stream->setCommand($this->command); $this->transport = new SmtpTransport($this->stream, $dispatcher, $logger); } } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { if ($this->transport) { return $this->transport->send($message, $envelope); } return parent::send($message, $envelope); } public function __toString(): string { if ($this->transport) { return (string) $this->transport; } return 'smtp://sendmail'; } protected function doSend(SentMessage $message): void { $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); $command = $this->command; if ($recipients = $message->getEnvelope()->getRecipients()) { $command = str_replace(' -t', '', $command); } if (!str_contains($command, ' -f')) { $command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress()); } $chunks = AbstractStream::replace("\r\n", "\n", $message->toIterable()); if (!str_contains($command, ' -i') && !str_contains($command, ' -oi')) { $chunks = AbstractStream::replace("\n.", "\n..", $chunks); } foreach ($recipients as $recipient) { $command .= ' '.escapeshellarg($recipient->getEncodedAddress()); } $this->stream->setCommand($command); $this->stream->initialize(); foreach ($chunks as $chunk) { $this->stream->write($chunk); } $this->stream->flush(); $this->stream->terminate(); $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); } } Transport/Transports.php 0000644 00000004407 15025057122 0011434 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ final class Transports implements TransportInterface { /** * @var array<string, TransportInterface> */ private array $transports = []; private $default; /** * @param iterable<string, TransportInterface> $transports */ public function __construct(iterable $transports) { foreach ($transports as $name => $transport) { $this->default ??= $transport; $this->transports[$name] = $transport; } if (!$this->transports) { throw new LogicException(sprintf('"%s" must have at least one transport configured.', __CLASS__)); } } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { /** @var Message $message */ if (RawMessage::class === \get_class($message) || !$message->getHeaders()->has('X-Transport')) { return $this->default->send($message, $envelope); } $headers = $message->getHeaders(); $transport = $headers->get('X-Transport')->getBody(); $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)))); } try { return $this->transports[$transport]->send($message, $envelope); } catch (\Throwable $e) { $headers->addTextHeader('X-Transport', $transport); throw $e; } } public function __toString(): string { return '['.implode(',', array_keys($this->transports)).']'; } } Transport/RoundRobinTransport.php 0000644 00000007157 15025057122 0013260 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\RawMessage; /** * Uses several Transports using a round robin algorithm. * * @author Fabien Potencier <fabien@symfony.com> */ 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) { if (!$transports) { 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 { $exception = null; while ($transport = $this->getNextTransport()) { try { return $transport->send($message, $envelope); } catch (TransportExceptionInterface $e) { $exception ??= new TransportException('All transports failed.'); $exception->appendDebug(sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug())); $this->deadTransports[$transport] = microtime(true); } } throw $exception ?? new TransportException('No transports found.'); } public function __toString(): string { return $this->getNameSymbol().'('.implode(' ', array_map('strval', $this->transports)).')'; } /** * Rotates the transport list around and returns the first instance. */ protected function getNextTransport(): ?TransportInterface { if (-1 === $this->cursor) { $this->cursor = $this->getInitialCursor(); } $cursor = $this->cursor; while (true) { $transport = $this->transports[$cursor]; if (!$this->isTransportDead($transport)) { break; } if ((microtime(true) - $this->deadTransports[$transport]) > $this->retryPeriod) { $this->deadTransports->detach($transport); break; } if ($this->cursor === $cursor = $this->moveCursor($cursor)) { return null; } } $this->cursor = $this->moveCursor($cursor); return $transport; } protected function isTransportDead(TransportInterface $transport): bool { return $this->deadTransports->contains($transport); } protected function getInitialCursor(): int { // the cursor initial value is randomized so that // when are not in a daemon, we are still rotating the transports return mt_rand(0, \count($this->transports) - 1); } protected function getNameSymbol(): string { return 'roundrobin'; } private function moveCursor(int $cursor): int { return ++$cursor >= \count($this->transports) ? 0 : $cursor; } } Transport/AbstractTransport.php 0000644 00000005465 15025057122 0012742 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ abstract class AbstractTransport implements TransportInterface { private $dispatcher; private $logger; private float $rate = 0; private float $lastSent = 0; public function __construct(EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { $this->dispatcher = $dispatcher; $this->logger = $logger ?? new NullLogger(); } /** * Sets the maximum number of messages to send per second (0 to disable). * * @return $this */ public function setMaxPerSecond(float $rate): static { if (0 >= $rate) { $rate = 0; } $this->rate = $rate; $this->lastSent = 0; return $this; } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { $message = clone $message; $envelope = null !== $envelope ? clone $envelope : Envelope::create($message); if (null !== $this->dispatcher) { $event = new MessageEvent($message, $envelope, (string) $this); $this->dispatcher->dispatch($event); $envelope = $event->getEnvelope(); $message = $event->getMessage(); } $message = new SentMessage($message, $envelope); $this->doSend($message); $this->checkThrottling(); return $message; } abstract protected function doSend(SentMessage $message): void; /** * @param Address[] $addresses * * @return string[] */ protected function stringifyAddresses(array $addresses): array { return array_map(function (Address $a) { return $a->toString(); }, $addresses); } protected function getLogger(): LoggerInterface { return $this->logger; } private function checkThrottling() { if (0 == $this->rate) { return; } $sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent); if (0 < $sleep) { $this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep)); usleep($sleep * 1000000); } $this->lastSent = microtime(true); } } Transport/AbstractHttpTransport.php 0000644 00000004177 15025057122 0013601 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Mailer\Exception\HttpTransportException; use Symfony\Component\Mailer\SentMessage; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Victor Bocharsky <victor@symfonycasts.com> */ abstract class AbstractHttpTransport extends AbstractTransport { protected $host; protected $port; protected $client; public function __construct(HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { $this->client = $client; 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__)); } $this->client = HttpClient::create(); } parent::__construct($dispatcher, $logger); } /** * @return $this */ public function setHost(?string $host): static { $this->host = $host; return $this; } /** * @return $this */ public function setPort(?int $port): static { $this->port = $port; return $this; } abstract protected function doSendHttp(SentMessage $message): ResponseInterface; protected function doSend(SentMessage $message): void { $response = null; try { $response = $this->doSendHttp($message); $message->appendDebug($response->getInfo('debug') ?? ''); } catch (HttpTransportException $e) { $e->appendDebug($e->getResponse()->getInfo('debug') ?? ''); throw $e; } } } Transport/Dsn.php 0000644 00000004662 15025057122 0010004 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\InvalidArgumentException; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ 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, 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 static function fromString(string $dsn): self { if (false === $parsedDsn = parse_url($dsn)) { throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn)); } if (!isset($parsedDsn['scheme'])) { throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a scheme.', $dsn)); } if (!isset($parsedDsn['host'])) { throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a host (use "default" by default).', $dsn)); } $user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; $password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; $port = $parsedDsn['port'] ?? null; parse_str($parsedDsn['query'] ?? '', $query); return new self($parsedDsn['scheme'], $parsedDsn['host'], $user, $password, $port, $query); } public function getScheme(): string { return $this->scheme; } public function getHost(): string { return $this->host; } public function getUser(): ?string { return $this->user; } public function getPassword(): ?string { return $this->password; } public function getPort(int $default = null): ?int { return $this->port ?? $default; } public function getOption(string $key, mixed $default = null) { return $this->options[$key] ?? $default; } } Transport/NativeTransportFactory.php 0000644 00000004003 15025057122 0013740 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; /** * Factory that configures a transport (sendmail or SMTP) based on php.ini settings. * * @author Laurent VOULLEMIER <laurent.voullemier@gmail.com> */ final class NativeTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) { throw new UnsupportedSchemeException($dsn, 'native', $this->getSupportedSchemes()); } if ($sendMailPath = ini_get('sendmail_path')) { return new SendmailTransport($sendMailPath, $this->dispatcher, $this->logger); } if ('\\' !== \DIRECTORY_SEPARATOR) { throw new TransportException('sendmail_path is not configured in php.ini.'); } // Only for windows hosts; at this point non-windows // host have already thrown an exception or returned a transport $host = ini_get('SMTP'); $port = (int) ini_get('smtp_port'); if (!$host || !$port) { throw new TransportException('smtp or smtp_port is not configured in php.ini.'); } $socketStream = new SocketStream(); $socketStream->setHost($host); $socketStream->setPort($port); if (465 !== $port) { $socketStream->disableTls(); } return new SmtpTransport($socketStream, $this->dispatcher, $this->logger); } protected function getSupportedSchemes(): array { return ['native']; } } Transport/SendmailTransportFactory.php 0000644 00000001707 15025057122 0014256 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ final class SendmailTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { if ('sendmail+smtp' === $dsn->getScheme() || 'sendmail' === $dsn->getScheme()) { return new SendmailTransport($dsn->getOption('command'), $this->dispatcher, $this->logger); } throw new UnsupportedSchemeException($dsn, 'sendmail', $this->getSupportedSchemes()); } protected function getSupportedSchemes(): array { return ['sendmail', 'sendmail+smtp']; } } Transport/FailoverTransport.php 0000644 00000001667 15025057122 0012746 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; /** * Uses several Transports using a failover algorithm. * * @author Fabien Potencier <fabien@symfony.com> */ class FailoverTransport extends RoundRobinTransport { private $currentTransport = null; protected function getNextTransport(): ?TransportInterface { if (null === $this->currentTransport || $this->isTransportDead($this->currentTransport)) { $this->currentTransport = parent::getNextTransport(); } return $this->currentTransport; } protected function getInitialCursor(): int { return 0; } protected function getNameSymbol(): string { return 'failover'; } } Transport/TransportFactoryInterface.php 0000644 00000001317 15025057122 0014417 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Exception\IncompleteDsnException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ interface TransportFactoryInterface { /** * @throws UnsupportedSchemeException * @throws IncompleteDsnException */ public function create(Dsn $dsn): TransportInterface; public function supports(Dsn $dsn): bool; } Transport/TransportInterface.php 0000644 00000001621 15025057122 0013065 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\RawMessage; /** * Interface for all mailer transports. * * When sending emails, you should prefer MailerInterface implementations * as they allow asynchronous sending. * * @author Fabien Potencier <fabien@symfony.com> */ interface TransportInterface { /** * @throws TransportExceptionInterface */ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage; public function __toString(): string; } Transport/Smtp/EsmtpTransportFactory.php 0000644 00000004316 15025057122 0014534 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp; use Symfony\Component\Mailer\Transport\AbstractTransportFactory; use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; use Symfony\Component\Mailer\Transport\TransportInterface; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ final class EsmtpTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { $tls = 'smtps' === $dsn->getScheme() ? true : null; $port = $dsn->getPort(0); $host = $dsn->getHost(); $transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger); if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOLEAN)) { /** @var SocketStream $stream */ $stream = $transport->getStream(); $streamOptions = $stream->getStreamOptions(); $streamOptions['ssl']['verify_peer'] = false; $streamOptions['ssl']['verify_peer_name'] = false; $stream->setStreamOptions($streamOptions); } if ($user = $dsn->getUser()) { $transport->setUsername($user); } if ($password = $dsn->getPassword()) { $transport->setPassword($password); } if (null !== ($localDomain = $dsn->getOption('local_domain'))) { $transport->setLocalDomain($localDomain); } if (null !== ($restartThreshold = $dsn->getOption('restart_threshold'))) { $transport->setRestartThreshold((int) $restartThreshold, (int) $dsn->getOption('restart_threshold_sleep', 0)); } if (null !== ($pingThreshold = $dsn->getOption('ping_threshold'))) { $transport->setPingThreshold((int) $pingThreshold); } return $transport; } protected function getSupportedSchemes(): array { return ['smtp', 'smtps']; } } Transport/Smtp/Stream/SocketStream.php 0000644 00000010765 15025057122 0014043 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting remote sockets. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn * * @internal */ final class SocketStream extends AbstractStream { private string $url; private string $host = 'localhost'; private int $port = 465; private float $timeout; private bool $tls = true; private ?string $sourceIp = null; private array $streamContextOptions = []; /** * @return $this */ public function setTimeout(float $timeout): static { $this->timeout = $timeout; return $this; } public function getTimeout(): float { return $this->timeout ?? (float) \ini_get('default_socket_timeout'); } /** * Literal IPv6 addresses should be wrapped in square brackets. * * @return $this */ public function setHost(string $host): static { $this->host = $host; return $this; } public function getHost(): string { return $this->host; } /** * @return $this */ public function setPort(int $port): static { $this->port = $port; return $this; } public function getPort(): int { return $this->port; } /** * Sets the TLS/SSL on the socket (disables STARTTLS). * * @return $this */ public function disableTls(): static { $this->tls = false; return $this; } public function isTLS(): bool { return $this->tls; } /** * @return $this */ public function setStreamOptions(array $options): static { $this->streamContextOptions = $options; return $this; } public function getStreamOptions(): array { return $this->streamContextOptions; } /** * Sets the source IP. * * IPv6 addresses should be wrapped in square brackets. * * @return $this */ public function setSourceIp(string $ip): static { $this->sourceIp = $ip; return $this; } /** * Returns the IP used to connect to the destination. */ public function getSourceIp(): ?string { return $this->sourceIp; } public function initialize(): void { $this->url = $this->host.':'.$this->port; if ($this->tls) { $this->url = 'ssl://'.$this->url; } $options = []; if ($this->sourceIp) { $options['socket']['bindto'] = $this->sourceIp.':0'; } if ($this->streamContextOptions) { $options = array_merge($options, $this->streamContextOptions); } // do it unconditionally as it will be used by STARTTLS as well if supported $options['ssl']['crypto_method'] = $options['ssl']['crypto_method'] ?? \STREAM_CRYPTO_METHOD_TLS_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; $streamContext = stream_context_create($options); $timeout = $this->getTimeout(); set_error_handler(function ($type, $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); } finally { restore_error_handler(); } stream_set_blocking($this->stream, true); stream_set_timeout($this->stream, $timeout); $this->in = &$this->stream; $this->out = &$this->stream; } public function startTLS(): bool { set_error_handler(function ($type, $msg) { throw new TransportException('Unable to connect with STARTTLS: '.$msg); }); try { return stream_socket_enable_crypto($this->stream, true); } finally { restore_error_handler(); } } public function terminate(): void { if (null !== $this->stream) { fclose($this->stream); } parent::terminate(); } protected function getReadConnectionDescription(): string { return $this->url; } } Transport/Smtp/Stream/AbstractStream.php 0000644 00000006760 15025057122 0014356 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting remote sockets and local processes. * * @author Fabien Potencier <fabien@symfony.com> * @author Nicolas Grekas <p@tchwork.com> * @author Chris Corbyn * * @internal */ abstract class AbstractStream { protected $stream; protected $in; protected $out; private string $debug = ''; public function write(string $bytes, bool $debug = true): void { if ($debug) { foreach (explode("\n", trim($bytes)) as $line) { $this->debug .= sprintf("> %s\n", $line); } } $bytesToWrite = \strlen($bytes); $totalBytesWritten = 0; while ($totalBytesWritten < $bytesToWrite) { $bytesWritten = @fwrite($this->in, substr($bytes, $totalBytesWritten)); if (false === $bytesWritten || 0 === $bytesWritten) { throw new TransportException('Unable to write bytes on the wire.'); } $totalBytesWritten += $bytesWritten; } } /** * Flushes the contents of the stream (empty it) and set the internal pointer to the beginning. */ public function flush(): void { fflush($this->in); } /** * Performs any initialization needed. */ abstract public function initialize(): void; public function terminate(): void { $this->stream = $this->out = $this->in = null; } public function readLine(): string { if (feof($this->out)) { return ''; } $line = fgets($this->out); 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())); } if ($metas['eof']) { throw new TransportException(sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription())); } } $this->debug .= sprintf('< %s', $line); return $line; } public function getDebug(): string { $debug = $this->debug; $this->debug = ''; return $debug; } public static function replace(string $from, string $to, iterable $chunks): \Generator { if ('' === $from) { yield from $chunks; return; } $carry = ''; $fromLen = \strlen($from); foreach ($chunks as $chunk) { if ('' === $chunk = $carry.$chunk) { continue; } if (str_contains($chunk, $from)) { $chunk = explode($from, $chunk); $carry = array_pop($chunk); yield implode($to, $chunk).$to; } else { $carry = $chunk; } if (\strlen($carry) > $fromLen) { yield substr($carry, 0, -$fromLen); $carry = substr($carry, -$fromLen); } } if ('' !== $carry) { yield $carry; } } abstract protected function getReadConnectionDescription(): string; } Transport/Smtp/Stream/ProcessStream.php 0000644 00000003033 15025057122 0014217 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting local processes. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn * * @internal */ final class ProcessStream extends AbstractStream { private string $command; public function setCommand(string $command) { $this->command = $command; } public function initialize(): void { $descriptorSpec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $pipes = []; $this->stream = proc_open($this->command, $descriptorSpec, $pipes); stream_set_blocking($pipes[2], false); if ($err = stream_get_contents($pipes[2])) { throw new TransportException('Process could not be started: '.$err); } $this->in = &$pipes[0]; $this->out = &$pipes[1]; } public function terminate(): void { if (null !== $this->stream) { fclose($this->in); fclose($this->out); proc_close($this->stream); } parent::terminate(); } protected function getReadConnectionDescription(): string { return 'process '.$this->command; } } Transport/Smtp/EsmtpTransport.php 0000644 00000014127 15025057122 0013205 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; /** * Sends Emails over SMTP with ESMTP support. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn */ class EsmtpTransport extends SmtpTransport { private array $authenticators = []; private string $username = ''; private string $password = ''; public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { parent::__construct(null, $dispatcher, $logger); // order is important here (roughly most secure and popular first) $this->authenticators = [ new Auth\CramMd5Authenticator(), new Auth\LoginAuthenticator(), new Auth\PlainAuthenticator(), new Auth\XOAuth2Authenticator(), ]; /** @var SocketStream $stream */ $stream = $this->getStream(); if (null === $tls) { if (465 === $port) { $tls = true; } else { $tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host; } } if (!$tls) { $stream->disableTls(); } if (0 === $port) { $port = $tls ? 465 : 25; } $stream->setHost($host); $stream->setPort($port); } /** * @return $this */ public function setUsername(string $username): static { $this->username = $username; return $this; } public function getUsername(): string { return $this->username; } /** * @return $this */ public function setPassword(string $password): static { $this->password = $password; return $this; } public function getPassword(): string { return $this->password; } public function addAuthenticator(AuthenticatorInterface $authenticator): void { $this->authenticators[] = $authenticator; } protected function doHeloCommand(): void { if (!$capabilities = $this->callHeloCommand()) { return; } /** @var SocketStream $stream */ $stream = $this->getStream(); // WARNING: !$stream->isTLS() is right, 100% sure :) // if you think that the ! should be removed, read the code again // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) { $this->executeCommand("STARTTLS\r\n", [220]); if (!$stream->startTLS()) { throw new TransportException('Unable to connect with STARTTLS.'); } $capabilities = $this->callHeloCommand(); } if (\array_key_exists('AUTH', $capabilities)) { $this->handleAuth($capabilities['AUTH']); } } private function callHeloCommand(): array { try { $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); } catch (TransportExceptionInterface $e) { try { parent::doHeloCommand(); return []; } catch (TransportExceptionInterface $ex) { if (!$ex->getCode()) { throw $e; } throw $ex; } } $capabilities = []; $lines = explode("\r\n", trim($response)); array_shift($lines); foreach ($lines as $line) { if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { $value = strtoupper(ltrim($matches[2], ' =')); $capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : []; } } return $capabilities; } private function handleAuth(array $modes): void { if (!$this->username) { return; } $authNames = []; $errors = []; $modes = array_map('strtolower', $modes); foreach ($this->authenticators as $authenticator) { if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) { continue; } $authNames[] = $authenticator->getAuthKeyword(); try { $authenticator->authenticate($this); return; } catch (TransportExceptionInterface $e) { try { $this->executeCommand("RSET\r\n", [250]); } catch (TransportExceptionInterface $_) { // ignore this exception as it probably means that the server error was final } // keep the error message, but tries the other authenticators $errors[$authenticator->getAuthKeyword()] = $e->getMessage(); } } if (!$authNames) { throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes))); } $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); } throw new TransportException($message); } } Transport/Smtp/SmtpTransport.php 0000644 00000025661 15025057122 0013045 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; use Symfony\Component\Mime\RawMessage; /** * Sends emails over SMTP. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn */ class SmtpTransport extends AbstractTransport { private bool $started = false; private int $restartThreshold = 100; private int $restartThresholdSleep = 0; private int $restartCounter = 0; private int $pingThreshold = 100; private float $lastMessageTime = 0; private $stream; private string $domain = '[127.0.0.1]'; public function __construct(AbstractStream $stream = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { parent::__construct($dispatcher, $logger); $this->stream = $stream ?? new SocketStream(); } public function getStream(): AbstractStream { return $this->stream; } /** * Sets the maximum number of messages to send before re-starting the transport. * * By default, the threshold is set to 100 (and no sleep at restart). * * @param int $threshold The maximum number of messages (0 to disable) * @param int $sleep The number of seconds to sleep between stopping and re-starting the transport * * @return $this */ public function setRestartThreshold(int $threshold, int $sleep = 0): static { $this->restartThreshold = $threshold; $this->restartThresholdSleep = $sleep; return $this; } /** * Sets the minimum number of seconds required between two messages, before the server is pinged. * If the transport wants to send a message and the time since the last message exceeds the specified threshold, * the transport will ping the server first (NOOP command) to check if the connection is still alive. * Otherwise the message will be sent without pinging the server first. * * Do not set the threshold too low, as the SMTP server may drop the connection if there are too many * non-mail commands (like pinging the server with NOOP). * * By default, the threshold is set to 100 seconds. * * @param int $seconds The minimum number of seconds between two messages required to ping the server * * @return $this */ public function setPingThreshold(int $seconds): static { $this->pingThreshold = $seconds; return $this; } /** * Sets the name of the local domain that will be used in HELO. * * This should be a fully-qualified domain name and should be truly the domain * you're using. * * If your server does not have a domain name, use the IP address. This will * automatically be wrapped in square brackets as described in RFC 5321, * section 4.1.3. * * @return $this */ public function setLocalDomain(string $domain): static { if ('' !== $domain && '[' !== $domain[0]) { if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { $domain = '['.$domain.']'; } elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { $domain = '[IPv6:'.$domain.']'; } } $this->domain = $domain; return $this; } /** * Gets the name of the domain that will be used in HELO. * * If an IP address was specified, this will be returned wrapped in square * brackets as described in RFC 5321, section 4.1.3. */ public function getLocalDomain(): string { return $this->domain; } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { try { $message = parent::send($message, $envelope); } catch (TransportExceptionInterface $e) { if ($this->started) { try { $this->executeCommand("RSET\r\n", [250]); } catch (TransportExceptionInterface $_) { // ignore this exception as it probably means that the server error was final } } throw $e; } $this->checkRestartThreshold(); return $message; } public function __toString(): string { if ($this->stream instanceof SocketStream) { $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; } return $name; } return 'smtp://sendmail'; } /** * Runs a command against the stream, expecting the given response codes. * * @param int[] $codes * * @throws TransportException when an invalid response if received * * @internal */ public function executeCommand(string $command, array $codes): string { $this->stream->write($command); $response = $this->getFullResponse(); $this->assertResponseCode($response, $codes); return $response; } protected function doSend(SentMessage $message): void { if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) { $this->ping(); } if (!$this->started) { $this->start(); } try { $envelope = $message->getEnvelope(); $this->doMailFromCommand($envelope->getSender()->getEncodedAddress()); foreach ($envelope->getRecipients() as $recipient) { $this->doRcptToCommand($recipient->getEncodedAddress()); } $this->executeCommand("DATA\r\n", [354]); try { foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) { $this->stream->write($chunk, false); } $this->stream->flush(); } catch (TransportExceptionInterface $e) { throw $e; } catch (\Exception $e) { $this->stream->terminate(); $this->started = false; $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); throw $e; } $this->executeCommand("\r\n.\r\n", [250]); $message->appendDebug($this->stream->getDebug()); $this->lastMessageTime = microtime(true); } catch (TransportExceptionInterface $e) { $e->appendDebug($this->stream->getDebug()); $this->lastMessageTime = 0; throw $e; } } protected function doHeloCommand(): void { $this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]); } private function doMailFromCommand(string $address): void { $this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]); } private function doRcptToCommand(string $address): void { $this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]); } private function start(): void { if ($this->started) { return; } $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); $this->stream->initialize(); $this->assertResponseCode($this->getFullResponse(), [220]); $this->doHeloCommand(); $this->started = true; $this->lastMessageTime = 0; $this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__)); } private function stop(): void { if (!$this->started) { return; } $this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__)); try { $this->executeCommand("QUIT\r\n", [221]); } catch (TransportExceptionInterface $e) { } finally { $this->stream->terminate(); $this->started = false; $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); } } private function ping(): void { if (!$this->started) { return; } try { $this->executeCommand("NOOP\r\n", [250]); } catch (TransportExceptionInterface $e) { $this->stop(); } } /** * @throws TransportException if a response code is incorrect */ private function assertResponseCode(string $response, array $codes): void { if (!$codes) { throw new LogicException('You must set the expected response code.'); } [$code] = sscanf($response, '%3d'); $valid = \in_array($code, $codes); if (!$valid || !$response) { $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code'; $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : ''; throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0); } } private function getFullResponse(): string { $response = ''; do { $line = $this->stream->readLine(); $response .= $line; } while ($line && isset($line[3]) && ' ' !== $line[3]); return $response; } private function checkRestartThreshold(): void { // when using sendmail via non-interactive mode, the transport is never "started" if (!$this->started) { return; } ++$this->restartCounter; if ($this->restartCounter < $this->restartThreshold) { return; } $this->stop(); if (0 < $sleep = $this->restartThresholdSleep) { $this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep)); sleep($sleep); } $this->start(); $this->restartCounter = 0; } public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } public function __destruct() { $this->stop(); } } Transport/Smtp/Auth/LoginAuthenticator.php 0000644 00000001737 15025057122 0014707 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles LOGIN authentication. * * @author Chris Corbyn */ class LoginAuthenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'LOGIN'; } /** * {@inheritdoc} * * @see https://www.ietf.org/rfc/rfc4954.txt */ 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]); } } Transport/Smtp/Auth/XOAuth2Authenticator.php 0000644 00000002041 15025057122 0015056 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles XOAUTH2 authentication. * * @author xu.li<AthenaLightenedMyPath@gmail.com> * * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol */ class XOAuth2Authenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'XOAUTH2'; } /** * {@inheritdoc} * * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism */ public function authenticate(EsmtpTransport $client): void { $client->executeCommand('AUTH XOAUTH2 '.base64_encode('user='.$client->getUsername()."\1auth=Bearer ".$client->getPassword()."\1\1")."\r\n", [235]); } } Transport/Smtp/Auth/CramMd5Authenticator.php 0000644 00000003227 15025057122 0015063 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles CRAM-MD5 authentication. * * @author Chris Corbyn */ class CramMd5Authenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'CRAM-MD5'; } /** * {@inheritdoc} * * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void { $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]); } /** * Generates a CRAM-MD5 response from a server challenge. */ private function getResponse(string $secret, string $challenge): string { if (\strlen($secret) > 64) { $secret = pack('H32', md5($secret)); } if (\strlen($secret) < 64) { $secret = str_pad($secret, 64, \chr(0)); } $kipad = substr($secret, 0, 64) ^ str_repeat(\chr(0x36), 64); $kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64); $inner = pack('H32', md5($kipad.$challenge)); $digest = md5($kopad.$inner); return $digest; } } Transport/Smtp/Auth/PlainAuthenticator.php 0000644 00000001614 15025057122 0014674 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles PLAIN authentication. * * @author Chris Corbyn */ class PlainAuthenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'PLAIN'; } /** * {@inheritdoc} * * @see https://www.ietf.org/rfc/rfc4954.txt */ 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]); } } Transport/Smtp/Auth/AuthenticatorInterface.php 0000644 00000001477 15025057122 0015540 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Transport\Smtp\Auth; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * An Authentication mechanism. * * @author Chris Corbyn */ interface AuthenticatorInterface { /** * Tries to authenticate the user. * * @throws TransportExceptionInterface */ public function authenticate(EsmtpTransport $client): void; /** * Gets the name of the AUTH mechanism this Authenticator handles. */ public function getAuthKeyword(): string; } DataCollector/MessageDataCollector.php 0000644 00000002672 15025057122 0014030 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\Mailer\Event\MessageEvents; use Symfony\Component\Mailer\EventListener\MessageLoggerListener; /** * @author Fabien Potencier <fabien@symfony.com> */ final class MessageDataCollector extends DataCollector { private $events; public function __construct(MessageLoggerListener $logger) { $this->events = $logger->getEvents(); } /** * {@inheritdoc} */ public function collect(Request $request, Response $response, \Throwable $exception = null) { $this->data['events'] = $this->events; } public function getEvents(): MessageEvents { return $this->data['events']; } /** * @internal */ public function base64Encode(string $data): string { return base64_encode($data); } /** * {@inheritdoc} */ public function reset() { $this->data = []; } /** * {@inheritdoc} */ public function getName(): string { return 'mailer'; } } EventListener/EnvelopeListener.php 0000644 00000003653 15025057122 0013335 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Message; /** * Manipulates the Envelope of a Message. * * @author Fabien Potencier <fabien@symfony.com> */ class EnvelopeListener implements EventSubscriberInterface { private $sender = null; /** * @var Address[]|null */ private ?array $recipients = null; /** * @param array<Address|string> $recipients */ public function __construct(Address|string $sender = null, array $recipients = null) { if (null !== $sender) { $this->sender = Address::create($sender); } if (null !== $recipients) { $this->recipients = Address::createArray($recipients); } } public function onMessage(MessageEvent $event): void { if ($this->sender) { $event->getEnvelope()->setSender($this->sender); $message = $event->getMessage(); if ($message instanceof Message) { if (!$message->getHeaders()->has('Sender') && !$message->getHeaders()->has('From')) { $message->getHeaders()->addMailboxHeader('Sender', $this->sender); } } } if ($this->recipients) { $event->getEnvelope()->setRecipients($this->recipients); } } public static function getSubscribedEvents(): array { return [ // should be the last one to allow header changes by other listeners first MessageEvent::class => ['onMessage', -255], ]; } } EventListener/MessageListener.php 0000644 00000007514 15025057122 0013144 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\RuntimeException; use Symfony\Component\Mime\BodyRendererInterface; use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Header\MailboxListHeader; use Symfony\Component\Mime\Message; /** * Manipulates the headers and the body of a Message. * * @author Fabien Potencier <fabien@symfony.com> */ class MessageListener implements EventSubscriberInterface { public const HEADER_SET_IF_EMPTY = 1; public const HEADER_ADD = 2; public const HEADER_REPLACE = 3; public const DEFAULT_RULES = [ 'from' => self::HEADER_SET_IF_EMPTY, 'return-path' => self::HEADER_SET_IF_EMPTY, 'reply-to' => self::HEADER_ADD, 'to' => self::HEADER_SET_IF_EMPTY, 'cc' => self::HEADER_ADD, 'bcc' => self::HEADER_ADD, ]; private $headers; private array $headerRules = []; private $renderer; public function __construct(Headers $headers = null, BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES) { $this->headers = $headers; $this->renderer = $renderer; foreach ($headerRules as $headerName => $rule) { $this->addHeaderRule($headerName, $rule); } } public function addHeaderRule(string $headerName, int $rule): void { if ($rule < 1 || $rule > 3) { throw new InvalidArgumentException(sprintf('The "%d" rule is not supported.', $rule)); } $this->headerRules[strtolower($headerName)] = $rule; } public function onMessage(MessageEvent $event): void { $message = $event->getMessage(); if (!$message instanceof Message) { return; } $this->setHeaders($message); $this->renderMessage($message); } private function setHeaders(Message $message): void { if (!$this->headers) { return; } $headers = $message->getHeaders(); foreach ($this->headers->all() as $name => $header) { if (!$headers->has($name)) { $headers->add($header); continue; } switch ($this->headerRules[$name] ?? self::HEADER_SET_IF_EMPTY) { case self::HEADER_SET_IF_EMPTY: break; case self::HEADER_REPLACE: $headers->remove($name); $headers->add($header); break; case self::HEADER_ADD: if (!Headers::isUniqueHeader($name)) { $headers->add($header); break; } $h = $headers->get($name); if (!$h instanceof MailboxListHeader) { throw new RuntimeException(sprintf('Unable to set header "%s".', $name)); } Headers::checkHeaderClass($header); foreach ($header->getAddresses() as $address) { $h->addAddress($address); } } } } private function renderMessage(Message $message): void { if (!$this->renderer) { return; } $this->renderer->render($message); } public static function getSubscribedEvents(): array { return [ MessageEvent::class => 'onMessage', ]; } } EventListener/MessageLoggerListener.php 0000644 00000002336 15025057122 0014301 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Event\MessageEvents; use Symfony\Contracts\Service\ResetInterface; /** * Logs Messages. * * @author Fabien Potencier <fabien@symfony.com> */ class MessageLoggerListener implements EventSubscriberInterface, ResetInterface { private $events; public function __construct() { $this->events = new MessageEvents(); } /** * {@inheritdoc} */ public function reset() { $this->events = new MessageEvents(); } public function onMessage(MessageEvent $event): void { $this->events->add($event); } public function getEvents(): MessageEvents { return $this->events; } public static function getSubscribedEvents(): array { return [ MessageEvent::class => ['onMessage', -255], ]; } } composer.json 0000644 00000002116 15025057122 0007265 0 ustar 00 { "name": "symfony/mailer", "type": "library", "description": "Helps sending emails", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.0.2", "egulias/email-validator": "^2.1.10|^3|^4", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", "symfony/event-dispatcher": "^5.4|^6.0", "symfony/mime": "^5.4|^6.0", "symfony/service-contracts": "^1.1|^2|^3" }, "require-dev": { "symfony/http-client-contracts": "^1.1|^2|^3", "symfony/messenger": "^5.4|^6.0" }, "conflict": { "symfony/http-kernel": "<5.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Mailer\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } CHANGELOG.md 0000644 00000004633 15025057122 0006362 0 ustar 00 CHANGELOG ========= 6.0 --- * The `HttpTransportException` class takes a string at first argument 5.4 --- * Enable the mailer to operate on any PSR-14-compatible event dispatcher 5.3 --- * added the `mailer` monolog channel and set it on all transport definitions 5.2.0 ----- * added `NativeTransportFactory` to configure a transport based on php.ini settings * added `local_domain`, `restart_threshold`, `restart_threshold_sleep` and `ping_threshold` options for `smtp` * added `command` option for `sendmail` 4.4.0 ----- * [BC BREAK] changed the `NullTransport` DSN from `smtp://null` to `null://null` * [BC BREAK] renamed `SmtpEnvelope` to `Envelope`, renamed `DelayedSmtpEnvelope` to `DelayedEnvelope` * [BC BREAK] changed the syntax for failover and roundrobin DSNs Before: dummy://a || dummy://b (for failover) dummy://a && dummy://b (for roundrobin) After: failover(dummy://a dummy://b) roundrobin(dummy://a dummy://b) * added support for multiple transports on a `Mailer` instance * [BC BREAK] removed the `auth_mode` DSN option (it is now always determined automatically) * STARTTLS cannot be enabled anymore (it is used automatically if TLS is disabled and the server supports STARTTLS) * [BC BREAK] Removed the `encryption` DSN option (use `smtps` instead) * Added support for the `smtps` protocol (does the same as using `smtp` and port `465`) * Added PHPUnit constraints * Added `MessageDataCollector` * Added `MessageEvents` and `MessageLoggerListener` to allow collecting sent emails * [BC BREAK] `TransportInterface` has a new `__toString()` method * [BC BREAK] Classes `AbstractApiTransport` and `AbstractHttpTransport` moved under `Transport` sub-namespace. * [BC BREAK] Transports depend on `Symfony\Contracts\EventDispatcher\EventDispatcherInterface` instead of `Symfony\Component\EventDispatcher\EventDispatcherInterface`. * Added possibility to register custom transport for dsn by implementing `Symfony\Component\Mailer\Transport\TransportFactoryInterface` and tagging with `mailer.transport_factory` tag in DI. * Added `Symfony\Component\Mailer\Test\TransportFactoryTestCase` to ease testing custom transport factories. * Added `SentMessage::getDebug()` and `TransportExceptionInterface::getDebug` to help debugging * Made `MessageEvent` final * add DSN parameter `verify_peer` to disable TLS peer verification for SMTP transport 4.3.0 ----- * Added the component. Exception/TransportException.php 0000644 00000001206 15025057122 0013064 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ class TransportException extends RuntimeException implements TransportExceptionInterface { private string $debug = ''; public function getDebug(): string { return $this->debug; } public function appendDebug(string $debug): void { $this->debug .= $debug; } } Exception/LogicException.php 0000644 00000000645 15025057122 0012133 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ class LogicException extends \LogicException implements ExceptionInterface { } Exception/UnsupportedSchemeException.php 0000644 00000005550 15025057122 0014553 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; use Symfony\Component\Mailer\Bridge; use Symfony\Component\Mailer\Transport\Dsn; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ class UnsupportedSchemeException extends LogicException { private const SCHEME_TO_PACKAGE_MAP = [ 'gmail' => [ 'class' => Bridge\Google\Transport\GmailTransportFactory::class, 'package' => 'symfony/google-mailer', ], 'mailgun' => [ 'class' => Bridge\Mailgun\Transport\MailgunTransportFactory::class, 'package' => 'symfony/mailgun-mailer', ], 'mailjet' => [ 'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class, 'package' => 'symfony/mailjet-mailer', ], 'mandrill' => [ 'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class, 'package' => 'symfony/mailchimp-mailer', ], 'ohmysmtp' => [ 'class' => Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory::class, 'package' => 'symfony/oh-my-smtp-mailer', ], 'postmark' => [ 'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class, 'package' => 'symfony/postmark-mailer', ], 'sendgrid' => [ 'class' => Bridge\Sendgrid\Transport\SendgridTransportFactory::class, 'package' => 'symfony/sendgrid-mailer', ], 'sendinblue' => [ 'class' => Bridge\Sendinblue\Transport\SendinblueTransportFactory::class, 'package' => 'symfony/sendinblue-mailer', ], 'ses' => [ 'class' => Bridge\Amazon\Transport\SesTransportFactory::class, 'package' => 'symfony/amazon-mailer', ], ]; public function __construct(Dsn $dsn, string $name = null, array $supported = []) { $provider = $dsn->getScheme(); if (false !== $pos = strpos($provider, '+')) { $provider = substr($provider, 0, $pos); } $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'])); return; } $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)); } parent::__construct($message.'.'); } } Exception/TransportExceptionInterface.php 0000644 00000000772 15025057122 0014714 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ interface TransportExceptionInterface extends ExceptionInterface { public function getDebug(): string; public function appendDebug(string $debug): void; } Exception/InvalidArgumentException.php 0000644 00000000671 15025057122 0014166 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } Exception/ExceptionInterface.php 0000644 00000000720 15025057122 0012770 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * Exception interface for all exceptions thrown by the component. * * @author Fabien Potencier <fabien@symfony.com> */ interface ExceptionInterface extends \Throwable { } Exception/IncompleteDsnException.php 0000644 00000000635 15025057122 0013641 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ class IncompleteDsnException extends InvalidArgumentException { } Exception/HttpTransportException.php 0000644 00000001446 15025057122 0013732 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ class HttpTransportException extends TransportException { private $response; public function __construct(string $message, ResponseInterface $response, int $code = 0, \Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->response = $response; } public function getResponse(): ResponseInterface { return $this->response; } } Exception/RuntimeException.php 0000644 00000000651 15025057122 0012516 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Exception; /** * @author Fabien Potencier <fabien@symfony.com> */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } Messenger/SendEmailMessage.php 0000644 00000001505 15025057122 0012353 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Messenger; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ class SendEmailMessage { private $message; private $envelope; public function __construct(RawMessage $message, Envelope $envelope = null) { $this->message = $message; $this->envelope = $envelope; } public function getMessage(): RawMessage { return $this->message; } public function getEnvelope(): ?Envelope { return $this->envelope; } } Messenger/MessageHandler.php 0000644 00000001413 15025057122 0012065 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Messenger; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\TransportInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ class MessageHandler { private $transport; public function __construct(TransportInterface $transport) { $this->transport = $transport; } public function __invoke(SendEmailMessage $message): ?SentMessage { return $this->transport->send($message->getMessage(), $message->getEnvelope()); } } DelayedEnvelope.php 0000644 00000004626 15025057122 0010331 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Message; /** * @author Fabien Potencier <fabien@symfony.com> * * @internal */ final class DelayedEnvelope extends Envelope { private bool $senderSet = false; private bool $recipientsSet = false; private $message; public function __construct(Message $message) { $this->message = $message; } public function setSender(Address $sender): void { parent::setSender($sender); $this->senderSet = true; } public function getSender(): Address { if (!$this->senderSet) { parent::setSender(self::getSenderFromHeaders($this->message->getHeaders())); } return parent::getSender(); } public function setRecipients(array $recipients): void { parent::setRecipients($recipients); $this->recipientsSet = (bool) parent::getRecipients(); } /** * @return Address[] */ public function getRecipients(): array { if ($this->recipientsSet) { return parent::getRecipients(); } return self::getRecipientsFromHeaders($this->message->getHeaders()); } private static function getRecipientsFromHeaders(Headers $headers): array { $recipients = []; foreach (['to', 'cc', 'bcc'] as $name) { foreach ($headers->all($name) as $header) { foreach ($header->getAddresses() as $address) { $recipients[] = $address; } } } return $recipients; } private static function getSenderFromHeaders(Headers $headers): Address { if ($sender = $headers->get('Sender')) { return $sender->getAddress(); } if ($return = $headers->get('Return-Path')) { return $return->getAddress(); } if ($from = $headers->get('From')) { return $from->getAddresses()[0]; } throw new LogicException('Unable to determine the sender of the message.'); } } MailerInterface.php 0000644 00000001373 15025057122 0010312 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mime\RawMessage; /** * Interface for mailers able to send emails synchronous and/or asynchronous. * * Implementations must support synchronous and asynchronous sending. * * @author Fabien Potencier <fabien@symfony.com> */ interface MailerInterface { /** * @throws TransportExceptionInterface */ public function send(RawMessage $message, Envelope $envelope = null): void; } README.md 0000644 00000004115 15025057122 0006023 0 ustar 00 Mailer Component ================ The Mailer component helps sending emails. Getting Started --------------- ``` $ composer require symfony/mailer ``` ```php use Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mime\Email; $transport = Transport::fromDsn('smtp://localhost'); $mailer = new Mailer($transport); $email = (new Email()) ->from('hello@example.com') ->to('you@example.com') //->cc('cc@example.com') //->bcc('bcc@example.com') //->replyTo('fabien@example.com') //->priority(Email::PRIORITY_HIGH) ->subject('Time for Symfony Mailer!') ->text('Sending emails is fun again!') ->html('<p>See Twig integration for better HTML integration!</p>'); $mailer->send($email); ``` To enable the Twig integration of the Mailer, require `symfony/twig-bridge` and set up the `BodyRenderer`: ```php use Symfony\Bridge\Twig\Mime\BodyRenderer; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Mailer\EventListener\MessageListener; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mailer\Transport; use Twig\Environment as TwigEnvironment; $twig = new TwigEnvironment(...); $messageListener = new MessageListener(null, new BodyRenderer($twig)); $eventDispatcher = new EventDispatcher(); $eventDispatcher->addSubscriber($messageListener); $transport = Transport::fromDsn('smtp://localhost', $eventDispatcher); $mailer = new Mailer($transport, null, $eventDispatcher); $email = (new TemplatedEmail()) // ... ->htmlTemplate('emails/signup.html.twig') ->context([ 'expiration_date' => new \DateTime('+7 days'), 'username' => 'foo', ]) ; $mailer->send($email); ``` Resources --------- * [Documentation](https://symfony.com/doc/current/mailer.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [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) Test/Constraint/EmailCount.php 0000644 00000003537 15025057122 0012367 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; 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; } /** * {@inheritdoc} */ public function toString(): string { return sprintf('%shas %s "%d" emails', $this->transport ? $this->transport.' ' : '', $this->queued ? 'queued' : 'sent', $this->expectedValue); } /** * @param MessageEvents $events * * {@inheritdoc} */ protected function matches($events): bool { return $this->expectedValue === $this->countEmails($events); } /** * @param MessageEvents $events * * {@inheritdoc} */ protected function failureDescription($events): string { return sprintf('the Transport %s (%d %s)', $this->toString(), $this->countEmails($events), $this->queued ? 'queued' : 'sent'); } private function countEmails(MessageEvents $events): int { $count = 0; foreach ($events->getEvents($this->transport) as $event) { if ( ($this->queued && $event->isQueued()) || (!$this->queued && !$event->isQueued()) ) { ++$count; } } return $count; } } Test/Constraint/EmailIsQueued.php 0000644 00000001626 15025057122 0013020 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Test\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\Mailer\Event\MessageEvent; final class EmailIsQueued extends Constraint { /** * {@inheritdoc} */ public function toString(): string { return 'is queued'; } /** * @param MessageEvent $event * * {@inheritdoc} */ protected function matches($event): bool { return $event->isQueued(); } /** * @param MessageEvent $event * * {@inheritdoc} */ protected function failureDescription($event): string { return 'the Email '.$this->toString(); } } Test/TransportFactoryTestCase.php 0000644 00000006307 15025057122 0013161 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ 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; /** * A test case to ease testing Transport Factory. * * @author Konstantin Myakshin <molodchick@gmail.com> */ abstract class TransportFactoryTestCase extends TestCase { protected const USER = 'u$er'; protected const PASSWORD = 'pa$s'; protected $dispatcher; protected $client; protected $logger; abstract public function getFactory(): TransportFactoryInterface; abstract public function supportsProvider(): iterable; abstract public function createProvider(): iterable; public function unsupportedSchemeProvider(): iterable { return []; } public 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->dispatcher = $this->createMock(EventDispatcherInterface::class); } protected function getClient(): HttpClientInterface { return $this->client ?? $this->client = $this->createMock(HttpClientInterface::class); } protected function getLogger(): LoggerInterface { return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class); } } LICENSE 0000644 00000002051 15025057122 0005546 0 ustar 00 Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Transport.php 0000644 00000015104 15025057122 0007251 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\FailoverTransport; use Symfony\Component\Mailer\Transport\NativeTransportFactory; use Symfony\Component\Mailer\Transport\NullTransportFactory; use Symfony\Component\Mailer\Transport\RoundRobinTransport; use Symfony\Component\Mailer\Transport\SendmailTransportFactory; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; use Symfony\Component\Mailer\Transport\TransportFactoryInterface; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mailer\Transport\Transports; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Fabien Potencier <fabien@symfony.com> * @author Konstantin Myakshin <molodchick@gmail.com> */ final class Transport { private const FACTORY_CLASSES = [ GmailTransportFactory::class, MailgunTransportFactory::class, MailjetTransportFactory::class, MandrillTransportFactory::class, OhMySmtpTransportFactory::class, PostmarkTransportFactory::class, SendgridTransportFactory::class, SendinblueTransportFactory::class, SesTransportFactory::class, ]; private iterable $factories; public static function fromDsn(string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface { $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); return $factory->fromString($dsn); } public static function fromDsns(array $dsns, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface { $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); return $factory->fromStrings($dsns); } /** * @param TransportFactoryInterface[] $factories */ public function __construct(iterable $factories) { $this->factories = $factories; } public function fromStrings(array $dsns): Transports { $transports = []; foreach ($dsns as $name => $dsn) { $transports[$name] = $this->fromString($dsn); } return new Transports($transports); } public function fromString(string $dsn): TransportInterface { [$transport, $offset] = $this->parseDsn($dsn); if ($offset !== \strlen($dsn)) { throw new InvalidArgumentException(sprintf('The DSN has some garbage at the end: "%s".', substr($dsn, $offset))); } return $transport; } private function parseDsn(string $dsn, int $offset = 0): array { static $keywords = [ 'failover' => FailoverTransport::class, 'roundrobin' => RoundRobinTransport::class, ]; while (true) { foreach ($keywords as $name => $class) { $name .= '('; if ($name === substr($dsn, $offset, \strlen($name))) { $offset += \strlen($name) - 1; preg_match('{\(([^()]|(?R))*\)}A', $dsn, $matches, 0, $offset); if (!isset($matches[0])) { continue; } ++$offset; $args = []; while (true) { [$arg, $offset] = $this->parseDsn($dsn, $offset); $args[] = $arg; if (\strlen($dsn) === $offset) { break; } ++$offset; if (')' === $dsn[$offset - 1]) { break; } } return [new $class($args), $offset]; } } 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)))); } if ($pos = strcspn($dsn, ' )', $offset)) { return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset, $pos))), $offset + $pos]; } return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset))), \strlen($dsn)]; } } public function fromDsnObject(Dsn $dsn): TransportInterface { foreach ($this->factories as $factory) { if ($factory->supports($dsn)) { return $factory->create($dsn); } } throw new UnsupportedSchemeException($dsn); } /** * @return \Traversable<int, TransportFactoryInterface> */ public static function getDefaultFactories(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): \Traversable { foreach (self::FACTORY_CLASSES as $factoryClass) { if (class_exists($factoryClass)) { yield new $factoryClass($dispatcher, $client, $logger); } } yield new NullTransportFactory($dispatcher, $client, $logger); yield new SendmailTransportFactory($dispatcher, $client, $logger); yield new EsmtpTransportFactory($dispatcher, $client, $logger); yield new NativeTransportFactory($dispatcher, $client, $logger); } } Event/MessageEvent.php 0000644 00000002767 15025057122 0010737 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Event; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mime\RawMessage; use Symfony\Contracts\EventDispatcher\Event; /** * Allows the transformation of a Message and the Envelope before the email is sent. * * @author Fabien Potencier <fabien@symfony.com> */ final class MessageEvent extends Event { private $message; private $envelope; private string $transport; private bool $queued; 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 getMessage(): RawMessage { return $this->message; } public function setMessage(RawMessage $message): void { $this->message = $message; } public function getEnvelope(): Envelope { return $this->envelope; } public function setEnvelope(Envelope $envelope): void { $this->envelope = $envelope; } public function getTransport(): string { return $this->transport; } public function isQueued(): bool { return $this->queued; } } Event/MessageEvents.php 0000644 00000002762 15025057122 0011115 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Event; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ class MessageEvents { /** * @var MessageEvent[] */ private array $events = []; /** * @var array<string, bool> */ private array $transports = []; public function add(MessageEvent $event): void { $this->events[] = $event; $this->transports[$event->getTransport()] = true; } public function getTransports(): array { return array_keys($this->transports); } /** * @return MessageEvent[] */ public function getEvents(string $name = null): array { if (null === $name) { return $this->events; } $events = []; foreach ($this->events as $event) { if ($name === $event->getTransport()) { $events[] = $event; } } return $events; } /** * @return RawMessage[] */ public function getMessages(string $name = null): array { $events = $this->getEvents($name); $messages = []; foreach ($events as $event) { $messages[] = $event->getMessage(); } return $messages; } } Header/TagHeader.php 0000644 00000001043 15025057122 0010266 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Header; use Symfony\Component\Mime\Header\UnstructuredHeader; /** * @author Kevin Bond <kevinbond@gmail.com> */ final class TagHeader extends UnstructuredHeader { public function __construct(string $value) { parent::__construct('X-Tag', $value); } } Header/MetadataHeader.php 0000644 00000001303 15025057122 0011272 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Header; use Symfony\Component\Mime\Header\UnstructuredHeader; /** * @author Kevin Bond <kevinbond@gmail.com> */ final class MetadataHeader extends UnstructuredHeader { private string $key; public function __construct(string $key, string $value) { $this->key = $key; parent::__construct('X-Metadata-'.$key, $value); } public function getKey(): string { return $this->key; } } SentMessage.php 0000644 00000004042 15025057122 0007472 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ class SentMessage { private $original; private $raw; private $envelope; private string $messageId; private string $debug = ''; /** * @internal */ public function __construct(RawMessage $message, Envelope $envelope) { $message->ensureValidity(); $this->original = $message; $this->envelope = $envelope; if ($message instanceof Message) { $message = clone $message; $headers = $message->getHeaders(); if (!$headers->has('Message-ID')) { $headers->addIdHeader('Message-ID', $message->generateMessageId()); } $this->messageId = $headers->get('Message-ID')->getId(); $this->raw = new RawMessage($message->toIterable()); } else { $this->raw = $message; } } public function getMessage(): RawMessage { return $this->raw; } public function getOriginalMessage(): RawMessage { return $this->original; } public function getEnvelope(): Envelope { return $this->envelope; } public function setMessageId(string $id): void { $this->messageId = $id; } public function getMessageId(): string { return $this->messageId; } public function getDebug(): string { return $this->debug; } public function appendDebug(string $debug): void { $this->debug .= $debug; } public function toString(): string { return $this->raw->toString(); } public function toIterable(): iterable { return $this->raw->toIterable(); } } Envelope.php 0000644 00000005074 15025057122 0007037 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ class Envelope { private $sender; private array $recipients = []; /** * @param Address[] $recipients */ public function __construct(Address $sender, array $recipients) { $this->setSender($sender); $this->setRecipients($recipients); } public static function create(RawMessage $message): self { if (RawMessage::class === \get_class($message)) { throw new LogicException('Cannot send a RawMessage instance without an explicit Envelope.'); } return new DelayedEnvelope($message); } public function setSender(Address $sender): void { // 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())); } $this->sender = $sender; } /** * @return Address Returns a "mailbox" as specified by RFC 2822 * Must be converted to an "addr-spec" when used as a "MAIL FROM" value in SMTP (use getAddress()) */ public function getSender(): Address { return $this->sender; } /** * @param Address[] $recipients */ public function setRecipients(array $recipients): void { if (!$recipients) { throw new InvalidArgumentException('An envelope must have at least one recipient.'); } $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))); } $this->recipients[] = new Address($recipient->getAddress()); } } /** * @return Address[] */ public function getRecipients(): array { return $this->recipients; } }
| ver. 1.4 |
.
| PHP 8.1.32 | Generation time: 0 |
proxy
|
phpinfo
|
Settings