diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php index ed6e644a56982..1927a5d98c798 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -20,6 +20,7 @@ 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\MicrosoftGraph\Transport\MicrosoftGraphTransportFactory; use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory; @@ -78,6 +79,10 @@ ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.microsoftgraph', MicrosoftGraphTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.postmark', PostmarkTransportFactory::class) ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory') diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/.gitattributes b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/.gitignore b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md new file mode 100644 index 0000000000000..468c504deecb2 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.3.0 +----- + + * Added the bridge diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/SendMailException.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/SendMailException.php new file mode 100644 index 0000000000000..816d36331f715 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/SendMailException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception; + +class SendMailException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/SenderNotFoundException.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/SenderNotFoundException.php new file mode 100644 index 0000000000000..6627ec9ec626d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/SenderNotFoundException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception; + +class SenderNotFoundException extends SendMailException +{ +} diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/UnAuthorizedException.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/UnAuthorizedException.php new file mode 100644 index 0000000000000..2823064923eb8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/UnAuthorizedException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception; + +class UnAuthorizedException extends SendMailException +{ +} diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/LICENSE b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/LICENSE new file mode 100644 index 0000000000000..3ed9f412ce53d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-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. diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md new file mode 100644 index 0000000000000..84f20d22e0b7d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md @@ -0,0 +1,54 @@ +Microsoft Graph API Mailer +========================== + +Provides Microsoft Graph API integration for Symfony Mailer. + + +Prerequisites +------------- + +You will need to: + * Register an application in your Microsoft Azure portal, + * Grant this application the Microsoft Graph `Mail.Send` permission, + * Create a secret for that app. + + +Configuration example +--------------------- + +```env +# MAILER +MAILER_DSN=microsoft+graph://CLIENT_APP_ID:SECRET@default?tenant=TENANT_ID +``` + +If you need to use third parties operated or specific regions Microsoft services (China, US Government, etc.), you can specify Auth Endpoint and Graph Endpoint. + +```env +# MAILER e.g. for China +MAILER_DSN=microsoft+graph://CLIENT_APP_ID:SECRET@login.partner.microsoftonline.cn?tenant=TENANT_ID&graphEndpoint=https://microsoftgraph.chinacloudapi.cn +``` + +| | Authentication endpoint | Graph Endpoint | +|------------------------|------------------------------------------|-----------------------------------------| +| Global (default) | https://login.microsoftonline.com | https://graph.microsoft.com | +| US Government L4 | https://login.microsoftonline.us | https://graph.microsoft.us | +| US Government L5 (DOD) | https://login.microsoftonline.us | https://dod-graph.microsoft.us | +| China | https://login.partner.microsoftonline.cn | https://microsoftgraph.chinacloudapi.cn | + +More details can be found in the Microsoft documentation : + * [Auth Endpoints](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud#microsoft-entra-authentication-endpoints) + * [Graph Endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) + + +Troubleshooting +--------------- + +Beware that the sender email address needs to be an address of an account inside your tenant. + +Resources +--------- + + * [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) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php new file mode 100644 index 0000000000000..101e99cd7a9c2 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Tests\Transport; + +use Microsoft\Graph\Core\NationalCloud; +use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContext; +use Psr\Log\NullLogger; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Transport\MicrosoftGraphTransport; +use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Transport\MicrosoftGraphTransportFactory; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Test\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class MicrosoftGraphTransportFactoryTest extends TransportFactoryTestCase +{ + protected const TENANT = 'tenantId'; + + public function getFactory(): TransportFactoryInterface + { + return new MicrosoftGraphTransportFactory(null, new MockHttpClient(), new NullLogger()); + } + + public static function supportsProvider(): iterable + { + yield [ + new Dsn('microsoft+graph', 'default'), + true, + ]; + + yield [ + new Dsn('microsoft+graph', 'example.com'), + true, + ]; + } + + public static function createProvider(): iterable + { + yield [ + new Dsn('microsoft+graph', 'default', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::GLOBAL, new ClientCredentialContext(self::TENANT, self::USER, self::PASSWORD)), + ]; + + yield [ + new Dsn('microsoft+graph', 'germany', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::GERMANY, new ClientCredentialContext(self::TENANT, self::USER, self::PASSWORD)), + ]; + + yield [ + new Dsn('microsoft+graph', 'china', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::CHINA, new ClientCredentialContext(self::TENANT, self::USER, self::PASSWORD)), + ]; + + yield [ + new Dsn('microsoft+graph', 'us-gov', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::US_GOV, new ClientCredentialContext(self::TENANT, self::USER, self::PASSWORD)), + ]; + + yield [ + new Dsn('microsoft+graph', 'us-dod', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::US_DOD, new ClientCredentialContext(self::TENANT, self::USER, self::PASSWORD)), + ]; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield [ + new Dsn('microsoft+smtp', 'default', self::USER, self::PASSWORD), + 'The "microsoft+smtp" scheme is not supported; supported schemes for mailer "microsoft graph" are: "microsoft+graph".', + ]; + } + + public static function incompleteDsnProvider(): iterable + { + yield [new Dsn('microsoft+graph', 'default', self::USER)]; + + yield [new Dsn('microsoft+graph', 'default', self::USER, self::PASSWORD)]; + + yield [new Dsn('microsoft+graph', 'default', null, self::PASSWORD)]; + + yield [new Dsn('microsoft+graph', 'default', null, null)]; + } + + public function testInvalidDsnHost() + { + $factory = $this->getFactory(); + + $this->expectException(InvalidArgumentException::class); + $factory->create(new Dsn('microsoft+graph', 'some-wrong-national-cloud', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT])); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php new file mode 100644 index 0000000000000..f46336139f2f1 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Transport; + +use GuzzleHttp\Psr7\Stream; +use GuzzleHttp\Psr7\Utils; +use Microsoft\Graph\Generated\Models\BodyType; +use Microsoft\Graph\Generated\Models\EmailAddress; +use Microsoft\Graph\Generated\Models\FileAttachment; +use Microsoft\Graph\Generated\Models\ItemBody; +use Microsoft\Graph\Generated\Models\Message; +use Microsoft\Graph\Generated\Models\ODataErrors\ODataError; +use Microsoft\Graph\Generated\Models\Recipient; +use Microsoft\Graph\Generated\Users\Item\SendMail\SendMailPostRequestBody; +use Microsoft\Graph\GraphServiceClient; +use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContext; +use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception\SenderNotFoundException; +use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception\SendMailException; +use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception\UnAuthorizedException; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Header\ParameterizedHeader; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\RawMessage; + +class MicrosoftGraphTransport implements TransportInterface +{ + private GraphServiceClient $graphServiceClient; + + public function __construct( + string $nationalCloud, + ClientCredentialContext $clientCredentialContext, + ) { + $this->graphServiceClient = new GraphServiceClient($clientCredentialContext, [], $nationalCloud); + } + + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + $envelope = null !== $envelope ? clone $envelope : Envelope::create($message); + + if (!$message instanceof Email) { + throw new SendMailException(\sprintf("This mailer can only handle mails of class '%s' or it's subclasses, instance of '%s' passed", Email::class, $message::class)); + } + + $this->sendMail($message); + + return new SentMessage($message, $envelope); + } + + private function sendMail(Email $message): void + { + $message = $this->convertEmailToGraphMessage($message); + $body = new SendMailPostRequestBody(); + $body->setMessage($message); + // Make sure $senderAddress is the email of an account in the tenant + $senderAddress = $message->getFrom()->getEmailAddress()->getAddress(); + + try { + $this->graphServiceClient->users()->byUserId($senderAddress)->sendMail()->post($body)->wait(); + } catch (ODataError $error) { + if ('ErrorInvalidUser' === $error->getError()->getCode()) { + throw new SenderNotFoundException("Sender email address '.".$senderAddress."' could not be found when calling the Graph API. This is usually because the email address doesn't exist in the tenant.", 404, $error); + } + throw new SendMailException('Something went wrong while sending email.', $error->getCode(), $error); + } catch (\Exception $exception) { + if ('unauthorized_client' === $exception->getMessage()) { + throw new UnAuthorizedException('Unauthorized to send email. Check your credentials.', 401, $exception); + } + throw new SendMailException('Something went wrong while sending email.', 0, $exception); + } + } + + private function convertEmailToGraphMessage(Email $source): Message + { + $message = new Message(); + + // From + if (0 === \count($source->getFrom())) { + throw new SendMailException("Cannot send mail without 'From'."); + } + + $message->setFrom(self::convertAddressToGraphRecipient($source->getFrom()[0])); + + // To + $message->setToRecipients(array_map( + static fn (Address $address) => self::convertAddressToGraphRecipient($address), + $source->getTo() + )); + + // CC + $message->setCcRecipients(array_map( + static fn (Address $address) => self::convertAddressToGraphRecipient($address), + $source->getCc() + )); + + // BCC + $message->setBccRecipients(array_map( + static fn (Address $address) => self::convertAddressToGraphRecipient($address), + $source->getBcc() + )); + + // Subject & body + $message->setSubject($source->getSubject() ?? ''); + + $itemBody = new ItemBody(); + if ($source->getHtmlBody()) { + $itemBody->setContent((string) $source->getHtmlBody()); + $itemBody->setContentType(new BodyType(BodyType::HTML)); + } else { + $itemBody->setContent((string) $source->getTextBody()); + $itemBody->setContentType(new BodyType(BodyType::TEXT)); + } + + $message->setBody($itemBody); + $message->setAttachments(array_map( + static fn (DataPart $attachment) => self::convertAttachmentGraphAttachment($attachment), + $source->getAttachments() + )); + + return $message; + } + + private static function convertAddressToGraphRecipient(Address $source): Recipient + { + $recipient = new Recipient(); + $emailAddress = new EmailAddress(); + $emailAddress->setAddress($source->getAddress()); + $emailAddress->setName($source->getName()); + $recipient->setEmailAddress($emailAddress); + + return $recipient; + } + + private static function convertAttachmentGraphAttachment(DataPart $source): FileAttachment + { + $attachment = new FileAttachment(); + + $contentDisposition = $source->getPreparedHeaders()->get('content-disposition'); + \assert($contentDisposition instanceof ParameterizedHeader); + $filename = $contentDisposition->getParameter('filename'); + + $fileStream = Utils::streamFor($source->bodyToString()); + \assert($fileStream instanceof Stream); + + $attachment->setContentBytes($fileStream); + $attachment->setContentType($source->getMediaType().'/'.$source->getMediaSubtype()); + $attachment->setName($filename); + $attachment->setODataType('#microsoft.graph.fileAttachment'); + + return $attachment; + } + + public function __toString(): string + { + return 'microsoft_graph://oauth_mail'; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php new file mode 100644 index 0000000000000..7010f5caa7b9e --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Transport; + +use Microsoft\Graph\Core\NationalCloud; +use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContext; +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +final class MicrosoftGraphTransportFactory extends AbstractTransportFactory +{ + private const CLOUD_MAP = [ + 'default' => NationalCloud::GLOBAL, + 'germany' => NationalCloud::GERMANY, + 'china' => NationalCloud::CHINA, + 'us-dod' => NationalCloud::US_DOD, + 'us-gov' => NationalCloud::US_GOV, + ]; + + /** + * @return string[] + */ + protected function getSupportedSchemes(): array + { + return ['microsoft+graph']; + } + + public function create(Dsn $dsn): TransportInterface + { + if ('microsoft+graph' !== $dsn->getScheme()) { + throw new UnsupportedSchemeException($dsn, 'microsoft graph', $this->getSupportedSchemes()); + } + $tenantId = $dsn->getOption('tenant'); + if (null === $tenantId) { + throw new IncompleteDsnException("Transport 'microsoft+graph' requires the 'tenant' option."); + } + if (!isset(self::CLOUD_MAP[$dsn->getHost()])) { + throw new InvalidArgumentException(\sprintf("Transport 'microsoft+graph' one of these hosts : '%s'", implode(', ', self::CLOUD_MAP))); + } + + return new MicrosoftGraphTransport( + self::CLOUD_MAP[$dsn->getHost()], + new ClientCredentialContext( + $tenantId, + $this->getUser($dsn), + $this->getPassword($dsn) + ) + ); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/composer.json b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/composer.json new file mode 100644 index 0000000000000..fce27d3c5cf2b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/composer.json @@ -0,0 +1,43 @@ +{ + "name": "symfony/microsoft-graph-mailer", + "type": "symfony-mailer-bridge", + "description": "Symfony Microsoft Graph Mailer Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Kevin Nguyen", + "homepage": "https://github.com/nguyenk" + }, + { + "name": "The Coding Machine", + "homepage": "https://github.com/thecodingmachine" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1", + "symfony/mailer": "^5.4|^6.4|^7.0", + "microsoft/microsoft-graph": "^2.0.0", + "guzzlehttp/psr7": "^2.0" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.4|^7.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\MicrosoftGraph\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "config": { + "allow-plugins": { + "php-http/discovery": true + } + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist new file mode 100644 index 0000000000000..c1500a2ddbd42 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Tests + ./vendor + + + diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php index 8543ebbea09d1..2a5aa3803625d 100644 --- a/src/Symfony/Component/Mailer/Transport.php +++ b/src/Symfony/Component/Mailer/Transport.php @@ -21,6 +21,7 @@ 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\MicrosoftGraph\Transport\MicrosoftGraphTransportFactory; use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory; @@ -54,6 +55,7 @@ final class Transport MailgunTransportFactory::class, MailjetTransportFactory::class, MandrillTransportFactory::class, + MicrosoftGraphTransportFactory::class, OhMySmtpTransportFactory::class, PostmarkTransportFactory::class, ScalewayTransportFactory::class, @@ -64,14 +66,14 @@ final class Transport private iterable $factories; - public static function fromDsn(#[\SensitiveParameter] string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface + 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 + 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))); @@ -142,7 +144,7 @@ private function parseDsn(#[\SensitiveParameter] string $dsn, int $offset = 0): } if (preg_match('{(\w+)\(}A', $dsn, $matches, 0, $offset)) { - throw new InvalidArgumentException(sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords)))); + throw new InvalidArgumentException(\sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords)))); } if ($pos = strcspn($dsn, ' )', $offset)) { @@ -167,7 +169,7 @@ public function fromDsnObject(Dsn $dsn): TransportInterface /** * @return \Traversable */ - public static function getDefaultFactories(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): \Traversable + public static function getDefaultFactories(?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): \Traversable { foreach (self::FACTORY_CLASSES as $factoryClass) { if (class_exists($factoryClass)) { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy