From 5388c84dcedac15c5688958cf89b27e3380e00d1 Mon Sep 17 00:00:00 2001 From: Kevin NGUYEN Date: Fri, 10 Nov 2023 18:48:50 +0100 Subject: [PATCH 1/2] 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 2/2] 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