From 5388c84dcedac15c5688958cf89b27e3380e00d1 Mon Sep 17 00:00:00 2001 From: Kevin NGUYEN Date: Fri, 10 Nov 2023 18:48:50 +0100 Subject: [PATCH 01/13] initial commit --- composer.json | 1 + .../Bridge/MicrosoftGraph/.gitattributes | 4 + .../Mailer/Bridge/MicrosoftGraph/.gitignore | 3 + .../Mailer/Bridge/MicrosoftGraph/CHANGELOG.md | 7 + .../Exception/SendMailException.php | 16 ++ .../Exception/SenderNotFoundException.php | 16 ++ .../Exception/UnAuthorizedException.php | 16 ++ .../Mailer/Bridge/MicrosoftGraph/LICENSE | 19 ++ .../Mailer/Bridge/MicrosoftGraph/README.md | 54 ++++++ .../MicrosoftGraphTransportFactoryTest.php | 100 ++++++++++ .../Transport/MicrosoftGraphTransport.php | 172 ++++++++++++++++++ .../MicrosoftGraphTransportFactory.php | 72 ++++++++ .../Bridge/MicrosoftGraph/composer.json | 44 +++++ .../Bridge/MicrosoftGraph/phpunit.xml.dist | 29 +++ 14 files changed, 553 insertions(+) create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/.gitattributes create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/.gitignore create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/SendMailException.php create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/SenderNotFoundException.php create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Exception/UnAuthorizedException.php create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/LICENSE create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/composer.json create mode 100644 src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist diff --git a/composer.json b/composer.json index 6fb094c569fa8..20b44583dd88f 100644 --- a/composer.json +++ b/composer.json @@ -141,6 +141,7 @@ "league/html-to-markdown": "^5.0", "league/uri": "^6.5|^7.0", "masterminds/html5": "^2.7.2", + "microsoft/microsoft-graph": "^2", "monolog/monolog": "^1.25.1|^2", "nyholm/psr7": "^1.0", "pda/pheanstalk": "^4.0", 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..b465b6c2e3df6 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +6.4.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..d7ccc60b1534a --- /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) + * [Grpah Endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) + + +Troubleshooting +-------- +//TODO : erreur stack trace +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..6e4f29b48a690 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php @@ -0,0 +1,100 @@ + + * + * 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 Symfony\Component\Cache\Adapter\NullAdapter; +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(new NullAdapter()); + } + + 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, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + ]; + + yield [ + new Dsn('microsoft+graph', 'germany', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::GERMANY, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + ]; + + yield [ + new Dsn('microsoft+graph', 'china', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::CHINA, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + ]; + + yield [ + new Dsn('microsoft+graph', 'us-gov', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::US_GOV, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + ]; + + yield [ + new Dsn('microsoft+graph', 'us-dod', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), + new MicrosoftGraphTransport(NationalCloud::US_DOD, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + ]; + } + + 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(): void + { + $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..bcf538674fc0a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php @@ -0,0 +1,172 @@ + + * + * 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\Exception\ClientException; +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 Safe\Exceptions\JsonException; +use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception\SenderNotFoundException; +use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception\SendMailException; +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; +use Symfony\Contracts\Cache\CacheInterface; + +use function Safe\json_decode; + +class MicrosoftGraphTransport implements TransportInterface +{ + private GraphServiceClient $graphServiceClient; + + public function __construct( + private readonly string $nationalCloud, + private readonly string $tenantId, + private readonly string $clientId, + private readonly string $clientSecret, + private readonly CacheInterface $cache, + ) { + $tokenRequestContext = new ClientCredentialContext( + $this->tenantId, + $this->clientId, + $this->clientSecret + ); + $this->graphServiceClient = new GraphServiceClient($tokenRequestContext, [], $this->nationalCloud); + } + + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + { + $envelope = null !== $envelope ? clone $envelope : Envelope::create($message); + + if (!$message instanceof Email) { + throw new SendEmailError(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); + } + } + + private function convertEmailToGraphMessage(Email $source): Message + { + $message = new Message(); + + // From + if (0 === \count($source->getFrom())) { + throw new SendEmailError("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() ?? 'No subject'); + $itemBody = new ItemBody(); + $itemBody->setContent((string) $source->getHtmlBody()); + $itemBody->setContentType(new BodyType(BodyType::HTML)); + $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) + ->setContentType($source->getMediaType().'/'.$source->getMediaSubtype()) + ->setName($filename) + ->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..b5206a52db7c7 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php @@ -0,0 +1,72 @@ + + * + * 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 phpDocumentor\Reflection\Exception\PcreException; +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; +use Symfony\Contracts\Cache\CacheInterface; + +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, + ]; + + public function __construct( + private readonly CacheInterface $cache, + ) { + parent::__construct(); + } + + /** + * @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))); + } + + // This parses the MAILER_DSN containing Microsoft Graph API credentials + return new MicrosoftGraphTransport( + self::CLOUD_MAP[$dsn->getHost()], + $tenantId, + $this->getUser($dsn), + $this->getPassword($dsn), + $this->cache + ); + } +} 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..9aa3dc580499b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/composer.json @@ -0,0 +1,44 @@ +{ + "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", + "thecodingmachine/safe": "^2.5.0", + "microsoft/microsoft-graph": "^2.0.0", + "symfony/cache": "6.4.x-dev" + }, + "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..31b1f3e0ed7a9 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Tests + ./vendor + + + From 5dd6b58e1ac83668c92a71632b326a0d4c7e1881 Mon Sep 17 00:00:00 2001 From: Kevin NGUYEN Date: Fri, 10 Nov 2023 19:41:38 +0100 Subject: [PATCH 02/13] fix CI --- .../MicrosoftGraphTransportFactoryTest.php | 3 +-- .../Transport/MicrosoftGraphTransport.php | 16 ++++++---------- .../Transport/MicrosoftGraphTransportFactory.php | 11 +++++------ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php index 6e4f29b48a690..168e5f42bca0e 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php @@ -89,12 +89,11 @@ public static function incompleteDsnProvider(): iterable yield [new Dsn('microsoft+graph', 'default', null, null)]; } - public function testInvalidDsnHost(): void + 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 index bcf538674fc0a..7124443ca5b2e 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php @@ -13,7 +13,6 @@ namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Transport; -use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Psr7\Stream; use GuzzleHttp\Psr7\Utils; use Microsoft\Graph\Generated\Models\BodyType; @@ -26,7 +25,6 @@ use Microsoft\Graph\Generated\Users\Item\SendMail\SendMailPostRequestBody; use Microsoft\Graph\GraphServiceClient; use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContext; -use Safe\Exceptions\JsonException; use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception\SenderNotFoundException; use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Exception\SendMailException; use Symfony\Component\Mailer\Envelope; @@ -39,8 +37,6 @@ use Symfony\Component\Mime\RawMessage; use Symfony\Contracts\Cache\CacheInterface; -use function Safe\json_decode; - class MicrosoftGraphTransport implements TransportInterface { private GraphServiceClient $graphServiceClient; @@ -68,7 +64,6 @@ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessa throw new SendEmailError(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); @@ -85,7 +80,7 @@ private function sendMail(Email $message): void try { $this->graphServiceClient->users()->byUserId($senderAddress)->sendMail()->post($body)->wait(); } catch (ODataError $error) { - if ('ErrorInvalidUser' === $error->getError()->getCode()){ + 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); @@ -104,19 +99,19 @@ private function convertEmailToGraphMessage(Email $source): Message $message->setFrom(self::convertAddressToGraphRecipient($source->getFrom()[0])); // to - $message->setToRecipients(\array_map( + $message->setToRecipients(array_map( static fn (Address $address) => self::convertAddressToGraphRecipient($address), $source->getTo() )); // CC - $message->setCcRecipients(\array_map( + $message->setCcRecipients(array_map( static fn (Address $address) => self::convertAddressToGraphRecipient($address), $source->getCc() )); // BCC - $message->setBccRecipients(\array_map( + $message->setBccRecipients(array_map( static fn (Address $address) => self::convertAddressToGraphRecipient($address), $source->getBcc() )); @@ -128,7 +123,7 @@ private function convertEmailToGraphMessage(Email $source): Message $itemBody->setContentType(new BodyType(BodyType::HTML)); $message->setBody($itemBody); - $message->setAttachments(\array_map( + $message->setAttachments(array_map( static fn (DataPart $attachment) => self::convertAttachmentGraphAttachment($attachment), $source->getAttachments() )); @@ -143,6 +138,7 @@ private static function convertAddressToGraphRecipient(Address $source): Recipie $emailAddress->setAddress($source->getAddress()); $emailAddress->setName($source->getName()); $recipient->setEmailAddress($emailAddress); + return $recipient; } diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php index b5206a52db7c7..9ac0030b70b46 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php @@ -14,7 +14,6 @@ namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Transport; use Microsoft\Graph\Core\NationalCloud; -use phpDocumentor\Reflection\Exception\PcreException; use Symfony\Component\Mailer\Exception\IncompleteDsnException; use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; @@ -28,9 +27,9 @@ 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, + 'china' => NationalCloud::CHINA, + 'us-dod' => NationalCloud::US_DOD, + 'us-gov' => NationalCloud::US_GOV, ]; public function __construct( @@ -56,8 +55,8 @@ public function create(Dsn $dsn): TransportInterface 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))); + if (!isset(self::CLOUD_MAP[$dsn->getHost()])) { + throw new InvalidArgumentException(sprintf("Transport 'microsoft+graph' one of these hosts : '%s'", implode(', ', self::CLOUD_MAP))); } // This parses the MAILER_DSN containing Microsoft Graph API credentials From e086c8f99a649fd5654b6bbe2fc39be9225d545d Mon Sep 17 00:00:00 2001 From: creiner Date: Tue, 13 May 2025 08:57:03 +0200 Subject: [PATCH 03/13] Improved MicrosoftGraphMailer after comments - added mail text body handling - removed unused properties - added composer deps --- .../MicrosoftGraphTransportFactoryTest.php | 16 ++++--- .../Transport/MicrosoftGraphTransport.php | 43 +++++++++---------- .../MicrosoftGraphTransportFactory.php | 18 +++----- .../Bridge/MicrosoftGraph/composer.json | 3 +- .../Bridge/MicrosoftGraph/phpunit.xml.dist | 2 +- 5 files changed, 37 insertions(+), 45 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php index 168e5f42bca0e..101e99cd7a9c2 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Tests\Transport; use Microsoft\Graph\Core\NationalCloud; -use Symfony\Component\Cache\Adapter\NullAdapter; +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; @@ -26,7 +28,7 @@ class MicrosoftGraphTransportFactoryTest extends TransportFactoryTestCase public function getFactory(): TransportFactoryInterface { - return new MicrosoftGraphTransportFactory(new NullAdapter()); + return new MicrosoftGraphTransportFactory(null, new MockHttpClient(), new NullLogger()); } public static function supportsProvider(): iterable @@ -46,27 +48,27 @@ public static function createProvider(): iterable { yield [ new Dsn('microsoft+graph', 'default', self::USER, self::PASSWORD, null, ['tenant' => self::TENANT]), - new MicrosoftGraphTransport(NationalCloud::GLOBAL, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + 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, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + 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, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + 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, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + 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, self::TENANT, self::USER, self::PASSWORD, new NullAdapter()), + new MicrosoftGraphTransport(NationalCloud::US_DOD, new ClientCredentialContext(self::TENANT, self::USER, self::PASSWORD)), ]; } diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php index 7124443ca5b2e..40db153a3f3af 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php @@ -35,25 +35,16 @@ use Symfony\Component\Mime\Header\ParameterizedHeader; use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\RawMessage; -use Symfony\Contracts\Cache\CacheInterface; class MicrosoftGraphTransport implements TransportInterface { private GraphServiceClient $graphServiceClient; public function __construct( - private readonly string $nationalCloud, - private readonly string $tenantId, - private readonly string $clientId, - private readonly string $clientSecret, - private readonly CacheInterface $cache, + readonly string $nationalCloud, + readonly ClientCredentialContext $clientCredentialContext, ) { - $tokenRequestContext = new ClientCredentialContext( - $this->tenantId, - $this->clientId, - $this->clientSecret - ); - $this->graphServiceClient = new GraphServiceClient($tokenRequestContext, [], $this->nationalCloud); + $this->graphServiceClient = new GraphServiceClient($clientCredentialContext, [], $this->nationalCloud); } public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage @@ -61,7 +52,7 @@ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessa $envelope = null !== $envelope ? clone $envelope : Envelope::create($message); if (!$message instanceof Email) { - throw new SendEmailError(sprintf("This mailer can only handle mails of class '%s' or it's subclasses, instance of %s passed", Email::class, $message::class)); + 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); @@ -93,12 +84,12 @@ private function convertEmailToGraphMessage(Email $source): Message // From if (0 === \count($source->getFrom())) { - throw new SendEmailError("Cannot send mail without 'From'"); + throw new SendMailException("Cannot send mail without 'From'"); } $message->setFrom(self::convertAddressToGraphRecipient($source->getFrom()[0])); - // to + // To $message->setToRecipients(array_map( static fn (Address $address) => self::convertAddressToGraphRecipient($address), $source->getTo() @@ -117,12 +108,18 @@ private function convertEmailToGraphMessage(Email $source): Message )); // Subject & body - $message->setSubject($source->getSubject() ?? 'No subject'); + $message->setSubject($source->getSubject() ?? ''); + $itemBody = new ItemBody(); - $itemBody->setContent((string) $source->getHtmlBody()); - $itemBody->setContentType(new BodyType(BodyType::HTML)); - $message->setBody($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() @@ -153,10 +150,10 @@ private static function convertAttachmentGraphAttachment(DataPart $source): File $fileStream = Utils::streamFor($source->bodyToString()); \assert($fileStream instanceof Stream); - $attachment->setContentBytes($fileStream) - ->setContentType($source->getMediaType().'/'.$source->getMediaSubtype()) - ->setName($filename) - ->setODataType('#microsoft.graph.fileAttachment'); + $attachment->setContentBytes($fileStream); + $attachment->setContentType($source->getMediaType().'/'.$source->getMediaSubtype()); + $attachment->setName($filename); + $attachment->setODataType('#microsoft.graph.fileAttachment'); return $attachment; } diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php index 9ac0030b70b46..d1fc8a46fa670 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php @@ -14,13 +14,13 @@ 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; -use Symfony\Contracts\Cache\CacheInterface; final class MicrosoftGraphTransportFactory extends AbstractTransportFactory { @@ -32,12 +32,6 @@ final class MicrosoftGraphTransportFactory extends AbstractTransportFactory 'us-gov' => NationalCloud::US_GOV, ]; - public function __construct( - private readonly CacheInterface $cache, - ) { - parent::__construct(); - } - /** * @return string[] */ @@ -59,13 +53,13 @@ public function create(Dsn $dsn): TransportInterface throw new InvalidArgumentException(sprintf("Transport 'microsoft+graph' one of these hosts : '%s'", implode(', ', self::CLOUD_MAP))); } - // This parses the MAILER_DSN containing Microsoft Graph API credentials return new MicrosoftGraphTransport( self::CLOUD_MAP[$dsn->getHost()], - $tenantId, - $this->getUser($dsn), - $this->getPassword($dsn), - $this->cache + 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 index 9aa3dc580499b..fce27d3c5cf2b 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/composer.json @@ -22,9 +22,8 @@ "require": { "php": ">=8.1", "symfony/mailer": "^5.4|^6.4|^7.0", - "thecodingmachine/safe": "^2.5.0", "microsoft/microsoft-graph": "^2.0.0", - "symfony/cache": "6.4.x-dev" + "guzzlehttp/psr7": "^2.0" }, "require-dev": { "symfony/http-client": "^5.4|^6.4|^7.0" diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist index 31b1f3e0ed7a9..c1500a2ddbd42 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/phpunit.xml.dist @@ -1,7 +1,7 @@ Date: Tue, 13 May 2025 08:57:29 +0200 Subject: [PATCH 04/13] Added MicrosoftGraphTransportFactory to the mailer transports --- .../FrameworkBundle/Resources/config/mailer_transports.php | 5 +++++ src/Symfony/Component/Mailer/Transport.php | 2 ++ 2 files changed, 7 insertions(+) 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/Transport.php b/src/Symfony/Component/Mailer/Transport.php index 8543ebbea09d1..a032f55046697 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, From 2c0d2e9bfbe32916d364eda9d1381066e208bcdf Mon Sep 17 00:00:00 2001 From: creiner Date: Tue, 13 May 2025 09:15:24 +0200 Subject: [PATCH 05/13] fixed ci --- .../MicrosoftGraphTransportFactoryTest.php | 2 ++ .../Transport/MicrosoftGraphTransport.php | 14 +++++++------- .../Transport/MicrosoftGraphTransportFactory.php | 6 +++--- src/Symfony/Component/Mailer/Transport.php | 8 ++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php index 101e99cd7a9c2..7741bd7747a38 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php @@ -9,6 +9,8 @@ * file that was distributed with this source code. */ +declare(strict_types=1); + namespace Symfony\Component\Mailer\Bridge\MicrosoftGraph\Tests\Transport; use Microsoft\Graph\Core\NationalCloud; diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php index 40db153a3f3af..b6a72c5dd163b 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php @@ -1,7 +1,5 @@ graphServiceClient = new GraphServiceClient($clientCredentialContext, [], $this->nationalCloud); } - public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + 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)); + 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); @@ -72,9 +72,9 @@ private function sendMail(Email $message): void $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 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); + throw new SendMailException('Something went wrong while sending email.', $error->getCode(), $error); } } @@ -84,7 +84,7 @@ private function convertEmailToGraphMessage(Email $source): Message // From if (0 === \count($source->getFrom())) { - throw new SendMailException("Cannot send mail without 'From'"); + throw new SendMailException("Cannot send mail without 'From'."); } $message->setFrom(self::convertAddressToGraphRecipient($source->getFrom()[0])); diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php index d1fc8a46fa670..b9a7ebd82cf11 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php @@ -1,7 +1,5 @@ getHost()])) { - throw new InvalidArgumentException(sprintf("Transport 'microsoft+graph' one of these hosts : '%s'", implode(', ', self::CLOUD_MAP))); + throw new InvalidArgumentException(\sprintf("Transport 'microsoft+graph' one of these hosts : '%s'", implode(', ', self::CLOUD_MAP))); } return new MicrosoftGraphTransport( diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php index a032f55046697..2a5aa3803625d 100644 --- a/src/Symfony/Component/Mailer/Transport.php +++ b/src/Symfony/Component/Mailer/Transport.php @@ -66,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))); @@ -144,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)) { @@ -169,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)) { From ecb1f3fb580563a5c0be2e5eb3973572ce2793fc Mon Sep 17 00:00:00 2001 From: creiner Date: Tue, 13 May 2025 09:17:29 +0200 Subject: [PATCH 06/13] second round of ci fixes --- .../Tests/Transport/MicrosoftGraphTransportFactoryTest.php | 5 +++-- .../MicrosoftGraph/Transport/MicrosoftGraphTransport.php | 7 ++++--- .../Transport/MicrosoftGraphTransportFactory.php | 7 ++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php index 7741bd7747a38..b13c8ccec2d35 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php @@ -1,5 +1,8 @@ sendMail($message); diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php index b9a7ebd82cf11..e690089a95604 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php @@ -1,5 +1,8 @@ getOption('tenant'); if (null === $tenantId) { - throw new IncompleteDsnException("Transport 'microsoft+graph' requires the 'tenant' option"); + 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))); From 383a1dc3ceec88dc3229eccb7713e6efa26e21b1 Mon Sep 17 00:00:00 2001 From: creiner Date: Tue, 13 May 2025 09:18:57 +0200 Subject: [PATCH 07/13] Removed empty line --- .../Tests/Transport/MicrosoftGraphTransportFactoryTest.php | 1 - .../Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php | 1 - .../MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php | 1 - 3 files changed, 3 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php index b13c8ccec2d35..3e659c4563563 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php @@ -1,6 +1,5 @@ Date: Tue, 13 May 2025 09:20:54 +0200 Subject: [PATCH 08/13] Removed declare strict types --- .../Tests/Transport/MicrosoftGraphTransportFactoryTest.php | 2 -- .../Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php | 2 -- .../MicrosoftGraph/Transport/MicrosoftGraphTransportFactory.php | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php index 3e659c4563563..101e99cd7a9c2 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Tests/Transport/MicrosoftGraphTransportFactoryTest.php @@ -1,7 +1,5 @@ Date: Tue, 13 May 2025 09:30:06 +0200 Subject: [PATCH 09/13] fixed readme and changelog --- .../Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md | 2 +- src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md index b465b6c2e3df6..468c504deecb2 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/CHANGELOG.md @@ -1,7 +1,7 @@ CHANGELOG ========= -6.4.0 +7.3.0 ----- * Added the bridge diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md index d7ccc60b1534a..4c1ed7ca16611 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md @@ -36,12 +36,11 @@ MAILER_DSN=microsoft+graph://CLIENT_APP_ID:SECRET@login.partner.microsoftonline. 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) - * [Grpah Endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) + * [Graph Endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) Troubleshooting -------- -//TODO : erreur stack trace Beware that the sender email address needs to be an address of an account inside your tenant. From cc4448260259366e60c6f75d619855c40b8e214a Mon Sep 17 00:00:00 2001 From: creiner Date: Tue, 13 May 2025 14:13:24 +0200 Subject: [PATCH 10/13] removed ms-graph from main composer.json --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 20b44583dd88f..6fb094c569fa8 100644 --- a/composer.json +++ b/composer.json @@ -141,7 +141,6 @@ "league/html-to-markdown": "^5.0", "league/uri": "^6.5|^7.0", "masterminds/html5": "^2.7.2", - "microsoft/microsoft-graph": "^2", "monolog/monolog": "^1.25.1|^2", "nyholm/psr7": "^1.0", "pda/pheanstalk": "^4.0", From 17d2660489ee096daf4ddb1f48c1a3848329f927 Mon Sep 17 00:00:00 2001 From: creiner Date: Tue, 13 May 2025 14:10:33 +0200 Subject: [PATCH 11/13] Apply suggestions from code review Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> --- .../Component/Mailer/Bridge/MicrosoftGraph/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md index 4c1ed7ca16611..84f20d22e0b7d 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/README.md @@ -1,11 +1,12 @@ 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, @@ -13,7 +14,7 @@ You will need to: Configuration example ---------- +--------------------- ```env # MAILER @@ -40,9 +41,9 @@ More details can be found in the Microsoft documentation : Troubleshooting --------- -Beware that the sender email address needs to be an address of an account inside your tenant. +--------------- +Beware that the sender email address needs to be an address of an account inside your tenant. Resources --------- From 4908410e6817fdb67e3af99af880551cbdb0a259 Mon Sep 17 00:00:00 2001 From: creiner Date: Tue, 13 May 2025 14:14:06 +0200 Subject: [PATCH 12/13] Removed readonly --- .../MicrosoftGraph/Transport/MicrosoftGraphTransport.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php index 7c8b00f702f32..d47f65b46aa5d 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php @@ -39,10 +39,10 @@ class MicrosoftGraphTransport implements TransportInterface private GraphServiceClient $graphServiceClient; public function __construct( - readonly string $nationalCloud, - readonly ClientCredentialContext $clientCredentialContext, + string $nationalCloud, + ClientCredentialContext $clientCredentialContext, ) { - $this->graphServiceClient = new GraphServiceClient($clientCredentialContext, [], $this->nationalCloud); + $this->graphServiceClient = new GraphServiceClient($clientCredentialContext, [], $nationalCloud); } public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage From f1857f86097306a178dfc8800cfb8c1d15e24eae Mon Sep 17 00:00:00 2001 From: creiner Date: Tue, 13 May 2025 14:32:42 +0200 Subject: [PATCH 13/13] Improved error handling --- .../MicrosoftGraph/Transport/MicrosoftGraphTransport.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php index d47f65b46aa5d..f46336139f2f1 100644 --- a/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/MicrosoftGraph/Transport/MicrosoftGraphTransport.php @@ -25,6 +25,7 @@ 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; @@ -73,6 +74,11 @@ private function sendMail(Email $message): void 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); } } 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