Файловый менеджер - Редактировать - /home/clickysoft/public_html/jmapi5.clickysoft.net/mailer.tar
Назад
Envelope.php 0000644 00000005077 15021236150 0007036 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 Address $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 === $message::class) { 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; } } Event/MessageEvent.php 0000644 00000004735 15021236150 0010730 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\Mailer\Exception\LogicException; use Symfony\Component\Messenger\Stamp\StampInterface; 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 RawMessage $message; private Envelope $envelope; private string $transport; private bool $queued; private bool $rejected = false; /** @var StampInterface[] */ private array $stamps = []; public function __construct(RawMessage $message, Envelope $envelope, string $transport, bool $queued = false) { $this->message = $message; $this->envelope = $envelope; $this->transport = $transport; $this->queued = $queued; } public function 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; } public function isRejected(): bool { return $this->rejected; } public function reject(): void { $this->rejected = true; $this->stopPropagation(); } public function addStamp(StampInterface $stamp): void { if (!$this->queued) { throw new LogicException(sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__)); } $this->stamps[] = $stamp; } /** * @return StampInterface[] */ public function getStamps(): array { if (!$this->queued) { throw new LogicException(sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__)); } return $this->stamps; } } Event/MessageEvents.php 0000644 00000002764 15021236150 0011113 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; } } Event/SentMessageEvent.php 0000644 00000001175 15021236150 0011555 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\SentMessage; use Symfony\Contracts\EventDispatcher\Event; /** * @author Fabien Potencier <fabien@symfony.com> */ final class SentMessageEvent extends Event { public function __construct(private SentMessage $message) { } public function getMessage(): SentMessage { return $this->message; } } Event/FailedMessageEvent.php 0000644 00000001375 15021236150 0012032 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; use Symfony\Contracts\EventDispatcher\Event; /** * @author Fabien Potencier <fabien@symfony.com> */ final class FailedMessageEvent extends Event { public function __construct( private RawMessage $message, private \Throwable $error, ) { } public function getMessage(): RawMessage { return $this->message; } public function getError(): \Throwable { return $this->error; } } composer.json 0000644 00000002500 15021236150 0007256 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.1", "egulias/email-validator": "^2.1.10|^3|^4", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", "symfony/event-dispatcher": "^5.4|^6.0|^7.0", "symfony/mime": "^6.2|^7.0", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { "symfony/console": "^5.4|^6.0|^7.0", "symfony/http-client": "^5.4|^6.0|^7.0", "symfony/messenger": "^6.2|^7.0", "symfony/twig-bridge": "^6.2|^7.0" }, "conflict": { "symfony/http-client-contracts": "<2.5", "symfony/http-kernel": "<5.4", "symfony/messenger": "<6.2", "symfony/mime": "<6.2", "symfony/twig-bridge": "<6.2.1" }, "autoload": { "psr-4": { "Symfony\\Component\\Mailer\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } Messenger/SendEmailMessage.php 0000644 00000001533 15021236150 0012350 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 RawMessage $message; private ?Envelope $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 00000001436 15021236150 0012066 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 TransportInterface $transport; public function __construct(TransportInterface $transport) { $this->transport = $transport; } public function __invoke(SendEmailMessage $message): ?SentMessage { return $this->transport->send($message->getMessage(), $message->getEnvelope()); } } Test/TransportFactoryTestCase.php 0000644 00000006263 15021236150 0013156 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 static function supportsProvider(): iterable; abstract public static function createProvider(): iterable; public static function unsupportedSchemeProvider(): iterable { return []; } public static function incompleteDsnProvider(): iterable { return []; } /** * @dataProvider supportsProvider */ public function testSupports(Dsn $dsn, bool $supports) { $factory = $this->getFactory(); $this->assertSame($supports, $factory->supports($dsn)); } /** * @dataProvider createProvider */ public function testCreate(Dsn $dsn, TransportInterface $transport) { $factory = $this->getFactory(); $this->assertEquals($transport, $factory->create($dsn)); if (str_contains('smtp', $dsn->getScheme())) { $this->assertStringMatchesFormat($dsn->getScheme().'://%S'.$dsn->getHost().'%S', (string) $transport); } } /** * @dataProvider unsupportedSchemeProvider */ public function testUnsupportedSchemeException(Dsn $dsn, ?string $message = null) { $factory = $this->getFactory(); $this->expectException(UnsupportedSchemeException::class); if (null !== $message) { $this->expectExceptionMessage($message); } $factory->create($dsn); } /** * @dataProvider incompleteDsnProvider */ public function testIncompleteDsnException(Dsn $dsn) { $factory = $this->getFactory(); $this->expectException(IncompleteDsnException::class); $factory->create($dsn); } protected function getDispatcher(): EventDispatcherInterface { return $this->dispatcher ??= $this->createMock(EventDispatcherInterface::class); } protected function getClient(): HttpClientInterface { return $this->client ??= $this->createMock(HttpClientInterface::class); } protected function getLogger(): LoggerInterface { return $this->logger ??= $this->createMock(LoggerInterface::class); } } Test/Constraint/EmailIsQueued.php 0000644 00000001471 15021236150 0013012 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 { public function toString(): string { return 'is queued'; } /** * @param MessageEvent $event */ protected function matches($event): bool { return $event->isQueued(); } /** * @param MessageEvent $event */ protected function failureDescription($event): string { return 'the Email '.$this->toString(); } } Test/Constraint/EmailCount.php 0000644 00000003363 15021236150 0012360 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; } public function toString(): string { return sprintf('%shas %s "%d" emails', $this->transport ? $this->transport.' ' : '', $this->queued ? 'queued' : 'sent', $this->expectedValue); } /** * @param MessageEvents $events */ protected function matches($events): bool { return $this->expectedValue === $this->countEmails($events); } /** * @param MessageEvents $events */ 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; } } Command/MailerTestCommand.php 0000644 00000005015 15021236150 0012177 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\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mime\Email; /** * A console command to test Mailer transports. */ #[AsCommand(name: 'mailer:test', description: 'Test Mailer transports by sending an email')] final class MailerTestCommand extends Command { public function __construct(private TransportInterface $transport) { $this->transport = $transport; parent::__construct(); } protected function configure(): void { $this ->addArgument('to', InputArgument::REQUIRED, 'The recipient of the message') ->addOption('from', null, InputOption::VALUE_OPTIONAL, 'The sender of the message', 'from@example.org') ->addOption('subject', null, InputOption::VALUE_OPTIONAL, 'The subject of the message', 'Testing transport') ->addOption('body', null, InputOption::VALUE_OPTIONAL, 'The body of the message', 'Testing body') ->addOption('transport', null, InputOption::VALUE_OPTIONAL, 'The transport to be used') ->setHelp(<<<'EOF' The <info>%command.name%</info> command tests a Mailer transport by sending a simple email message: <info>php %command.full_name% to@example.com</info> You can also specify a specific transport: <info>php %command.full_name% to@example.com --transport=transport_name</info> Note that this command bypasses the Messenger bus if configured. EOF ); } protected function execute(InputInterface $input, OutputInterface $output): int { $message = (new Email()) ->to($input->getArgument('to')) ->from($input->getOption('from')) ->subject($input->getOption('subject')) ->text($input->getOption('body')) ; if ($transport = $input->getOption('transport')) { $message->getHeaders()->addTextHeader('X-Transport', $transport); } $this->transport->send($message); return 0; } } Mailer.php 0000644 00000005510 15021236150 0006462 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 TransportInterface $transport; private ?MessageBusInterface $bus; private ?EventDispatcherInterface $dispatcher; public function __construct(TransportInterface $transport, ?MessageBusInterface $bus = null, ?EventDispatcherInterface $dispatcher = null) { $this->transport = $transport; $this->bus = $bus; $this->dispatcher = $dispatcher; } public function send(RawMessage $message, ?Envelope $envelope = null): void { if (null === $this->bus) { $this->transport->send($message, $envelope); return; } $stamps = []; 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); $stamps = $event->getStamps(); if ($event->isRejected()) { return; } } try { $this->bus->dispatch(new SendEmailMessage($message, $envelope), $stamps); } catch (HandlerFailedException $e) { foreach ($e->getWrappedExceptions() as $nested) { if ($nested instanceof TransportExceptionInterface) { throw $nested; } } throw $e; } } } Exception/TransportExceptionInterface.php 0000644 00000000772 15021236150 0014710 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/RuntimeException.php 0000644 00000000651 15021236150 0012512 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 { } Exception/InvalidArgumentException.php 0000644 00000000671 15021236150 0014162 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/TransportException.php 0000644 00000001206 15021236150 0013060 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/IncompleteDsnException.php 0000644 00000000635 15021236150 0013635 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 00000001471 15021236150 0013724 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 ResponseInterface $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/ExceptionInterface.php 0000644 00000000720 15021236150 0012764 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/UnexpectedResponseException.php 0000644 00000000536 15021236150 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; class UnexpectedResponseException extends TransportException { } Exception/UnsupportedSchemeException.php 0000644 00000007257 15021236150 0014555 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 = [ 'brevo' => [ 'class' => Bridge\Brevo\Transport\BrevoTransportFactory::class, 'package' => 'symfony/brevo-mailer', ], 'gmail' => [ 'class' => Bridge\Google\Transport\GmailTransportFactory::class, 'package' => 'symfony/google-mailer', ], 'infobip' => [ 'class' => Bridge\Infobip\Transport\InfobipTransportFactory::class, 'package' => 'symfony/infobip-mailer', ], 'mailersend' => [ 'class' => Bridge\MailerSend\Transport\MailerSendTransportFactory::class, 'package' => 'symfony/mailersend-mailer', ], 'mailgun' => [ 'class' => Bridge\Mailgun\Transport\MailgunTransportFactory::class, 'package' => 'symfony/mailgun-mailer', ], 'mailjet' => [ 'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class, 'package' => 'symfony/mailjet-mailer', ], 'mailpace' => [ 'class' => Bridge\MailPace\Transport\MailPaceTransportFactory::class, 'package' => 'symfony/mail-pace-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', ], 'scaleway' => [ 'class' => Bridge\Scaleway\Transport\ScalewayTransportFactory::class, 'package' => 'symfony/scaleway-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/LogicException.php 0000644 00000000645 15021236150 0012127 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 { } CHANGELOG.md 0000644 00000006460 15021236150 0006356 0 ustar 00 CHANGELOG ========= 6.4 --- * Add DSN parameter `peer_fingerprint` to verify TLS certificate fingerprint * Change the default port for the `mailjet+smtp` transport from 465 to 587 6.3 --- * Add `MessageEvent::reject()` to allow rejecting an email before sending it * Change the default port for the `mailgun+smtp` transport from 465 to 587 * Add `$authenticators` parameter in `EsmtpTransport` constructor and `EsmtpTransport::setAuthenticators()` to allow overriding of default eSMTP authenticators 6.2.7 ----- * [BC BREAK] The following data providers for `TransportFactoryTestCase` are now static: `supportsProvider()`, `createProvider()`, `unsupportedSchemeProvider()`and `incompleteDsnProvider()` 6.2 --- * Add a `mailer:test` command * Add `SentMessageEvent` and `FailedMessageEvent` events 6.1 --- * Make `start()` and `stop()` methods public on `SmtpTransport` * Improve extensibility of `EsmtpTransport` 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. Transport.php 0000644 00000016375 15021236150 0007260 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\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory; use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory; 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 = [ BrevoTransportFactory::class, GmailTransportFactory::class, InfobipTransportFactory::class, MailerSendTransportFactory::class, MailgunTransportFactory::class, MailjetTransportFactory::class, MailPaceTransportFactory::class, MandrillTransportFactory::class, OhMySmtpTransportFactory::class, PostmarkTransportFactory::class, ScalewayTransportFactory::class, SendgridTransportFactory::class, SendinblueTransportFactory::class, SesTransportFactory::class, ]; private iterable $factories; public static function fromDsn(#[\SensitiveParameter] string $dsn, ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): TransportInterface { $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); return $factory->fromString($dsn); } public static function fromDsns(#[\SensitiveParameter] 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(#[\SensitiveParameter] array $dsns): Transports { $transports = []; foreach ($dsns as $name => $dsn) { $transports[$name] = $this->fromString($dsn); } return new Transports($transports); } public function fromString(#[\SensitiveParameter] string $dsn): TransportInterface { [$transport, $offset] = $this->parseDsn($dsn); if ($offset !== \strlen($dsn)) { throw new InvalidArgumentException('The mailer DSN has some garbage at the end.'); } return $transport; } private function parseDsn(#[\SensitiveParameter] 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); } } EventListener/EnvelopeListener.php 0000644 00000003672 15021236150 0013332 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 ?Address $sender = null; /** * @var Address[]|null */ private ?array $recipients = null; /** * @param array<Address|string> $recipients */ public function __construct(Address|string|null $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 00000007556 15021236150 0013146 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 $headers; private array $headerRules = []; private ?BodyRendererInterface $renderer; public function __construct(?Headers $headers = null, ?BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES) { $this->headers = $headers; $this->renderer = $renderer; 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/MessengerTransportListener.php 0000644 00000002636 15021236150 0015421 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\Messenger\Stamp\TransportNamesStamp; use Symfony\Component\Mime\Message; /** * Allows messages to be sent to specific Messenger transports via the "X-Bus-Transport" MIME header. * * @author Fabien Potencier <fabien@symfony.com> */ final class MessengerTransportListener implements EventSubscriberInterface { public function onMessage(MessageEvent $event): void { if (!$event->isQueued()) { return; } $message = $event->getMessage(); if (!$message instanceof Message || !$message->getHeaders()->has('X-Bus-Transport')) { return; } $names = $message->getHeaders()->get('X-Bus-Transport')->getBody(); $names = array_map('trim', explode(',', $names)); $event->addStamp(new TransportNamesStamp($names)); $message->getHeaders()->remove('X-Bus-Transport'); } public static function getSubscribedEvents(): array { return [ MessageEvent::class => 'onMessage', ]; } } EventListener/MessageLoggerListener.php 0000644 00000002353 15021236150 0014274 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 MessageEvents $events; public function __construct() { $this->events = new MessageEvents(); } /** * @return void */ 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], ]; } } DelayedEnvelope.php 0000644 00000004636 15021236150 0010326 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 $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.'); } } DataCollector/MessageDataCollector.php 0000644 00000002546 15021236150 0014024 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 MessageEvents $events; public function __construct(MessageLoggerListener $logger) { $this->events = $logger->getEvents(); } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->data['events'] = $this->events; } public function getEvents(): MessageEvents { return $this->data['events']; } /** * @internal */ public function base64Encode(string $data): string { return base64_encode($data); } public function reset(): void { $this->data = []; } public function getName(): string { return 'mailer'; } } Header/TagHeader.php 0000644 00000001043 15021236150 0010262 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 15021236150 0011266 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; } } Transport/TransportFactoryInterface.php 0000644 00000001317 15021236150 0014413 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/RoundRobinTransport.php 0000644 00000007160 15021236150 0013246 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/NullTransport.php 0000644 00000001216 15021236150 0012073 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/AbstractApiTransport.php 0000644 00000003052 15021236150 0013356 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()); } /** * @return Address[] */ protected function getRecipients(Email $email, Envelope $envelope): array { return array_filter($envelope->getRecipients(), fn (Address $address) => false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true)); } } Transport/Smtp/SmtpTransport.php 0000644 00000030074 15021236150 0013033 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\Exception\UnexpectedResponseException; 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 AbstractStream $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; } protected function parseMessageId(string $mtaResult): string { $regexps = [ '/250 Ok (?P<id>[0-9a-f-]+)\r?$/mis', '/250 Ok:? queued as (?P<id>[A-Z0-9]+)\r?$/mis', ]; $matches = []; foreach ($regexps as $regexp) { if (preg_match($regexp, $mtaResult, $matches)) { return $matches['id']; } } return ''; } 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 */ 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; } $mtaResult = $this->executeCommand("\r\n.\r\n", [250]); $message->appendDebug($this->stream->getDebug()); $this->lastMessageTime = microtime(true); if ($mtaResult && $messageId = $this->parseMessageId($mtaResult)) { $message->setMessageId($messageId); } } catch (TransportExceptionInterface $e) { $e->appendDebug($this->stream->getDebug()); $this->lastMessageTime = 0; throw $e; } } /** * @internal since version 6.1, to be made private in 7.0 * * @final since version 6.1, to be made private in 7.0 */ 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]); } public 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__)); } /** * Manually disconnect from the SMTP server. * * In most cases this is not necessary since the disconnect happens automatically on termination. * In cases of long-running scripts, this might however make sense to avoid keeping an open * connection to the SMTP server in between sending emails. */ public 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) { } 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) { $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 UnexpectedResponseException(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__); } /** * @return void */ public function __wakeup() { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } public function __destruct() { $this->stop(); } } Transport/Smtp/EsmtpTransportFactory.php 0000644 00000004766 15021236150 0014541 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); /** @var SocketStream $stream */ $stream = $transport->getStream(); $streamOptions = $stream->getStreamOptions(); if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOL)) { $streamOptions['ssl']['verify_peer'] = false; $streamOptions['ssl']['verify_peer_name'] = false; } if (null !== $peerFingerprint = $dsn->getOption('peer_fingerprint')) { $streamOptions['ssl']['peer_fingerprint'] = $peerFingerprint; } $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 !== ($maxPerSecond = $dsn->getOption('max_per_second'))) { $transport->setMaxPerSecond((float) $maxPerSecond); } 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 00000011010 15021236150 0014017 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'] ??= \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, (int) $timeout, (int) (($timeout - (int) $timeout) * 1000000)); $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 00000007503 15021236150 0014346 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 { /** @var resource|null */ protected $stream; /** @var resource|null */ protected $in; /** @var resource|null */ protected $out; protected $err; 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->err = $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())); } if (false === $line) { throw new TransportException(sprintf('Unable to read from connection to "%s": ', $this->getReadConnectionDescription()).error_get_last()['message']); } } $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 00000004213 15021236150 0014214 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; private bool $interactive = false; public function setCommand(string $command): void { $this->command = $command; } public function setInteractive(bool $interactive): void { $this->interactive = $interactive; } public function initialize(): void { $descriptorSpec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', '\\' === \DIRECTORY_SEPARATOR ? 'a' : '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]; $this->err = &$pipes[2]; } public function terminate(): void { if (null !== $this->stream) { fclose($this->in); $out = stream_get_contents($this->out); fclose($this->out); $err = stream_get_contents($this->err); fclose($this->err); if (0 !== $exitCode = proc_close($this->stream)) { $errorMessage = 'Process failed with exit code '.$exitCode.': '.$out.$err; } } parent::terminate(); if (!$this->interactive && isset($errorMessage)) { throw new TransportException($errorMessage); } } protected function getReadConnectionDescription(): string { return 'process '.$this->command; } } Transport/Smtp/EsmtpTransport.php 0000644 00000016570 15021236150 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\Exception\UnexpectedResponseException; use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; 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 = ''; private array $capabilities; public function __construct(string $host = 'localhost', int $port = 0, ?bool $tls = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null, ?AbstractStream $stream = null, ?array $authenticators = null) { parent::__construct($stream, $dispatcher, $logger); if (null === $authenticators) { // fallback to default authenticators // order is important here (roughly most secure and popular first) $authenticators = [ new Auth\CramMd5Authenticator(), new Auth\LoginAuthenticator(), new Auth\PlainAuthenticator(), new Auth\XOAuth2Authenticator(), ]; } $this->setAuthenticators($authenticators); /** @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(#[\SensitiveParameter] string $password): static { $this->password = $password; return $this; } public function getPassword(): string { return $this->password; } public function setAuthenticators(array $authenticators): void { $this->authenticators = []; foreach ($authenticators as $authenticator) { $this->addAuthenticator($authenticator); } } public function addAuthenticator(AuthenticatorInterface $authenticator): void { $this->authenticators[] = $authenticator; } public function executeCommand(string $command, array $codes): string { return [250] === $codes && str_starts_with($command, 'HELO ') ? $this->doEhloCommand() : parent::executeCommand($command, $codes); } final protected function getCapabilities(): array { return $this->capabilities; } private function doEhloCommand(): string { try { $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); } catch (TransportExceptionInterface $e) { try { return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]); } catch (TransportExceptionInterface $ex) { if (!$ex->getCode()) { throw $e; } throw $ex; } } $this->capabilities = $this->parseCapabilities($response); /** @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', $this->capabilities)) { $this->executeCommand("STARTTLS\r\n", [220]); if (!$stream->startTLS()) { throw new TransportException('Unable to connect with STARTTLS.'); } $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); $this->capabilities = $this->parseCapabilities($response); } if (\array_key_exists('AUTH', $this->capabilities)) { $this->handleAuth($this->capabilities['AUTH']); } return $response; } private function parseCapabilities(string $ehloResponse): array { $capabilities = []; $lines = explode("\r\n", trim($ehloResponse)); 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; } $code = null; $authNames = []; $errors = []; $modes = array_map('strtolower', $modes); foreach ($this->authenticators as $authenticator) { if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) { continue; } $code = null; $authNames[] = $authenticator->getAuthKeyword(); try { $authenticator->authenticate($this); return; } catch (UnexpectedResponseException $e) { $code = $e->getCode(); 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)), $code ?: 504); } $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, $code ?: 535); } } Transport/Smtp/Auth/PlainAuthenticator.php 0000644 00000001560 15021236150 0014670 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'; } /** * @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/LoginAuthenticator.php 0000644 00000001703 15021236150 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 LOGIN authentication. * * @author Chris Corbyn */ class LoginAuthenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'LOGIN'; } /** * @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/AuthenticatorInterface.php 0000644 00000001477 15021236150 0015534 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; } Transport/Smtp/Auth/CramMd5Authenticator.php 0000644 00000003511 15021236150 0015053 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\InvalidArgumentException; 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'; } /** * @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(#[\SensitiveParameter] string $secret, string $challenge): string { if (!$secret) { throw new InvalidArgumentException('A non-empty secret is required.'); } 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/XOAuth2Authenticator.php 0000644 00000002005 15021236150 0015052 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'; } /** * @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/Transports.php 0000644 00000004426 15021236150 0011431 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 TransportInterface $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 === $message::class || !$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/SendmailTransportFactory.php 0000644 00000001707 15021236150 0014252 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/TransportInterface.php 0000644 00000001573 15021236150 0013067 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 extends \Stringable { /** * @throws TransportExceptionInterface */ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage; } Transport/AbstractTransport.php 0000644 00000007736 15021236150 0012741 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\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Event\FailedMessageEvent; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Event\SentMessageEvent; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\BodyRendererInterface; use Symfony\Component\Mime\RawMessage; /** * @author Fabien Potencier <fabien@symfony.com> */ abstract class AbstractTransport implements TransportInterface { private ?EventDispatcherInterface $dispatcher; private LoggerInterface $logger; private float $rate = 0; private float $lastSent = 0; public function __construct(?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) { $this->dispatcher = $dispatcher; $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); try { if (!$this->dispatcher) { $sentMessage = new SentMessage($message, $envelope); $this->doSend($sentMessage); return $sentMessage; } $event = new MessageEvent($message, $envelope, (string) $this); $this->dispatcher->dispatch($event); if ($event->isRejected()) { return null; } $envelope = $event->getEnvelope(); $message = $event->getMessage(); if ($message instanceof TemplatedEmail && !$message->isRendered()) { throw new LogicException(sprintf('You must configure a "%s" when a "%s" instance has a text or HTML template set.', BodyRendererInterface::class, get_debug_type($message))); } $sentMessage = new SentMessage($message, $envelope); try { $this->doSend($sentMessage); } catch (\Throwable $error) { $this->dispatcher->dispatch(new FailedMessageEvent($message, $error)); $this->checkThrottling(); throw $error; } $this->dispatcher->dispatch(new SentMessageEvent($sentMessage)); return $sentMessage; } finally { $this->checkThrottling(); } } abstract protected function doSend(SentMessage $message): void; /** * @param Address[] $addresses * * @return string[] */ protected function stringifyAddresses(array $addresses): array { return array_map(fn (Address $a) => $a->toString(), $addresses); } protected function getLogger(): LoggerInterface { return $this->logger; } private function checkThrottling(): void { 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((int) ($sleep * 1000000)); } $this->lastSent = microtime(true); } } Transport/AbstractHttpTransport.php 0000644 00000004150 15021236150 0013564 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 { 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 00000004624 15021236150 0007776 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, #[\SensitiveParameter] ?string $password = null, ?int $port = null, array $options = []) { $this->scheme = $scheme; $this->host = $host; $this->user = $user; $this->password = $password; $this->port = $port; $this->options = $options; } public static function fromString(#[\SensitiveParameter] string $dsn): self { if (false === $params = parse_url($dsn)) { throw new InvalidArgumentException('The mailer DSN is invalid.'); } if (!isset($params['scheme'])) { throw new InvalidArgumentException('The mailer DSN must contain a scheme.'); } if (!isset($params['host'])) { throw new InvalidArgumentException('The mailer DSN must contain a host (use "default" by default).'); } $user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null; $password = '' !== ($params['pass'] ?? '') ? rawurldecode($params['pass']) : null; $port = $params['port'] ?? null; parse_str($params['query'] ?? '', $query); return new self($params['scheme'], $params['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): mixed { return $this->options[$key] ?? $default; } } Transport/AbstractTransportFactory.php 0000644 00000002702 15021236150 0014255 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 { return $dsn->getUser() ?? throw new IncompleteDsnException('User is not set.'); } protected function getPassword(Dsn $dsn): string { return $dsn->getPassword() ?? throw new IncompleteDsnException('Password is not set.'); } } Transport/FailoverTransport.php 0000644 00000001713 15021236150 0012732 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 ?TransportInterface $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/NativeTransportFactory.php 0000644 00000004003 15021236150 0013734 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/SendmailTransport.php 0000644 00000010402 15021236150 0012712 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 ProcessStream $stream; private ?SmtpTransport $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->stream->setInteractive(true); $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/NullTransportFactory.php 0000644 00000001535 15021236150 0013427 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']; } } README.md 0000644 00000004126 15021236150 0006021 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 \DateTimeImmutable('+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) SentMessage.php 0000644 00000004101 15021236150 0007462 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 RawMessage $original; private RawMessage $raw; private Envelope $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(); } } LICENSE 0000644 00000002054 15021236150 0005545 0 ustar 00 Copyright (c) 2019-present 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. MailerInterface.php 0000644 00000001400 15021236150 0010275 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 synchronously and/or asynchronously. * * 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; }
| ver. 1.4 |
Github
|
.
| PHP 8.1.29 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка