Skip to content

Commit 088bd94

Browse files
committed
feature symfony#52842 [Mailer] Add Azure bridge (hafael)
This PR was squashed before being merged into the 7.1 branch. Discussion ---------- [Mailer] Add Azure bridge | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Docs | symfony/symfony-docs#19237 | Recipe | symfony/recipes#1265 | License | MIT closes symfony#52751 * symfony#52751 This PR adds a new mailer bridge for [Azure Communication Services Email](https://learn.microsoft.com/en-us/azure/communication-services/concepts/email/email-overview) Commits ------- dec3110 [Mailer] Add Azure bridge
2 parents c4e97eb + dec3110 commit 088bd94

File tree

13 files changed

+657
-0
lines changed

13 files changed

+657
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,6 +2559,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
25592559
}
25602560

25612561
$classToServices = [
2562+
MailerBridge\Azure\Transport\AzureTransportFactory::class => 'mailer.transport_factory.azure',
25622563
MailerBridge\Brevo\Transport\BrevoTransportFactory::class => 'mailer.transport_factory.brevo',
25632564
MailerBridge\Google\Transport\GmailTransportFactory::class => 'mailer.transport_factory.gmail',
25642565
MailerBridge\Infobip\Transport\InfobipTransportFactory::class => 'mailer.transport_factory.infobip',
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
vendor/
2+
composer.lock
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
7.1
5+
---
6+
7+
* Add the bridge
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2023-present Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Microsoft Azure Mailer
2+
======================
3+
4+
Provides [Azure Communication Services Email](https://learn.microsoft.com/en-us/azure/communication-services/concepts/email/email-overview) integration for Symfony Mailer.
5+
6+
Configuration example:
7+
8+
```env
9+
# API
10+
MAILER_DSN=azure+api://ACS_RESOURCE_NAME:KEY@default
11+
12+
#API with options
13+
14+
MAILER_DSN=azure+api://ACS_RESOURCE_NAME:KEY@default?api_version=2023-03-31&disable_tracking=false
15+
```
16+
17+
where:
18+
- `ACS_RESOURCE_NAME` is your Azure Communication Services endpoint resource name (https://ACS_RESOURCE_NAME.communication.azure.com)
19+
- `KEY` is your Azure Communication Services Email API Key
20+
21+
Resources
22+
---------
23+
24+
* [Microsoft Azure (ACS) Email API Docs](https://learn.microsoft.com/en-us/rest/api/communication/dataplane/email/send)
25+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
26+
* [Report issues](https://github.com/symfony/symfony/issues) and
27+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
28+
in the [main Symfony repository](https://github.com/symfony/symfony)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Bridge\Azure\Tests\Transport;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\HttpClient\Response\JsonMockResponse;
17+
use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureApiTransport;
18+
use Symfony\Component\Mailer\Envelope;
19+
use Symfony\Component\Mailer\Header\MetadataHeader;
20+
use Symfony\Component\Mailer\Header\TagHeader;
21+
use Symfony\Component\Mime\Address;
22+
use Symfony\Component\Mime\Email;
23+
use Symfony\Contracts\HttpClient\ResponseInterface;
24+
25+
class AzureApiTransportTest extends TestCase
26+
{
27+
/**
28+
* @dataProvider getTransportData
29+
*/
30+
public function testToString(AzureApiTransport $transport, string $expected)
31+
{
32+
$this->assertSame($expected, (string) $transport);
33+
}
34+
35+
public static function getTransportData(): array
36+
{
37+
return [
38+
[
39+
new AzureApiTransport('KEY', 'ACS_RESOURCE_NAME'),
40+
'azure+api://ACS_RESOURCE_NAME.communication.azure.com',
41+
],
42+
];
43+
}
44+
45+
public function testCustomHeader()
46+
{
47+
$email = new Email();
48+
$email->getHeaders()->addTextHeader('foo', 'bar');
49+
$envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]);
50+
51+
$transport = new AzureApiTransport('KEY', 'ACS_RESOURCE_NAME');
52+
$method = new \ReflectionMethod(AzureApiTransport::class, 'getPayload');
53+
$payload = $method->invoke($transport, $email, $envelope);
54+
55+
$this->assertArrayHasKey('headers', $payload);
56+
$this->assertArrayHasKey('foo', $payload['headers']);
57+
$this->assertEquals('bar', $payload['headers']['foo']);
58+
}
59+
60+
public function testSend()
61+
{
62+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
63+
$this->assertSame('POST', $method);
64+
$this->assertSame('https://my-acs-resource.communication.azure.com/emails:send?api-version=2023-03-31', $url);
65+
66+
$body = json_decode($options['body'], true);
67+
68+
$message = $body['content'];
69+
$this->assertSame('normal', $body['importance']);
70+
// $this->assertSame('Fabien', $message['from_name']);
71+
$this->assertSame('fabpot@symfony.com', $body['senderAddress']);
72+
$this->assertSame('Saif Eddin', $body['recipients']['to'][0]['displayName']);
73+
$this->assertSame('saif.gmati@symfony.com', $body['recipients']['to'][0]['address']);
74+
$this->assertSame('Hello!', $message['subject']);
75+
$this->assertSame('Hello There!', $message['plainText']);
76+
77+
return new JsonMockResponse([
78+
'id' => 'foobar',
79+
], [
80+
'http_code' => 202,
81+
]);
82+
});
83+
84+
$transport = new AzureApiTransport('KEY', 'my-acs-resource', true, '2023-03-31', $client);
85+
86+
$mail = new Email();
87+
$mail->subject('Hello!')
88+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
89+
->from(new Address('fabpot@symfony.com', 'Fabien'))
90+
->text('Hello There!');
91+
92+
$message = $transport->send($mail);
93+
94+
$this->assertSame('foobar', $message->getMessageId());
95+
}
96+
97+
public function testTagAndMetadataHeaders()
98+
{
99+
$email = new Email();
100+
$email->getHeaders()->add(new TagHeader('category-one'));
101+
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
102+
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));
103+
$envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]);
104+
105+
$transport = new AzureApiTransport('KEY', 'ACS_RESOURCE_NAME');
106+
$method = new \ReflectionMethod(AzureApiTransport::class, 'getPayload');
107+
$payload = $method->invoke($transport, $email, $envelope);
108+
109+
$this->assertArrayHasKey('headers', $payload);
110+
$this->assertArrayHasKey('X-Tag', $payload['headers']);
111+
$this->assertArrayHasKey('X-Metadata-Color', $payload['headers']);
112+
$this->assertArrayHasKey('X-Metadata-Client-ID', $payload['headers']);
113+
114+
$this->assertCount(3, $payload['headers']);
115+
116+
$this->assertSame('category-one', $payload['headers']['X-Tag']);
117+
$this->assertSame('blue', $payload['headers']['X-Metadata-Color']);
118+
$this->assertSame('12345', $payload['headers']['X-Metadata-Client-ID']);
119+
}
120+
121+
public function testItDoesNotAllowToAddResourceNameWithDot()
122+
{
123+
$this->expectException(\Exception::class);
124+
$this->expectExceptionMessage('Resource name cannot contain or end with a dot');
125+
126+
new AzureApiTransport('KEY', 'ACS_RESOURCE_NAME.');
127+
}
128+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Bridge\Azure\Tests\Transport;
13+
14+
use Psr\Log\NullLogger;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureApiTransport;
17+
use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureTransportFactory;
18+
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
19+
use Symfony\Component\Mailer\Transport\Dsn;
20+
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
21+
22+
class AzureTransportFactoryTest extends TransportFactoryTestCase
23+
{
24+
public function getFactory(): TransportFactoryInterface
25+
{
26+
return new AzureTransportFactory(null, new MockHttpClient(), new NullLogger());
27+
}
28+
29+
public static function supportsProvider(): iterable
30+
{
31+
yield [
32+
new Dsn('azure', 'default'),
33+
true,
34+
];
35+
36+
yield [
37+
new Dsn('azure+api', 'default'),
38+
true,
39+
];
40+
}
41+
42+
public static function createProvider(): iterable
43+
{
44+
yield [
45+
new Dsn('azure', 'default', self::USER, self::PASSWORD),
46+
new AzureApiTransport(self::PASSWORD, self::USER, false, '2023-03-31', new MockHttpClient(), null, new NullLogger()),
47+
];
48+
yield [
49+
new Dsn('azure', 'ACS_RESOURCE_NAME', self::USER, self::PASSWORD),
50+
(new AzureApiTransport(self::PASSWORD, self::USER, false, '2023-03-31', new MockHttpClient(), null, new NullLogger()))->setHost('ACS_RESOURCE_NAME'),
51+
];
52+
yield [
53+
new Dsn('azure+api', 'default', self::USER, self::PASSWORD),
54+
new AzureApiTransport(self::PASSWORD, self::USER, false, '2023-03-31', new MockHttpClient(), null, new NullLogger()),
55+
];
56+
yield [
57+
new Dsn('azure+api', 'ACS_RESOURCE_NAME', self::USER, self::PASSWORD),
58+
(new AzureApiTransport(self::PASSWORD, self::USER, false, '2023-03-31', new MockHttpClient(), null, new NullLogger()))->setHost('ACS_RESOURCE_NAME'),
59+
];
60+
}
61+
62+
public static function unsupportedSchemeProvider(): iterable
63+
{
64+
yield [
65+
new Dsn('azure+foo', 'default', self::USER, self::PASSWORD),
66+
'The "azure+foo" scheme is not supported; supported schemes for mailer "azure" are: "azure", "azure+api".',
67+
];
68+
}
69+
70+
public static function incompleteDsnProvider(): iterable
71+
{
72+
yield [new Dsn('azure', 'default')];
73+
yield [new Dsn('azure', 'default', self::USER)];
74+
yield [new Dsn('azure', 'default', null, self::PASSWORD)];
75+
yield [new Dsn('azure+api', 'default')];
76+
yield [new Dsn('azure+api', 'default', self::USER)];
77+
yield [new Dsn('azure+api', 'default', null, self::PASSWORD)];
78+
}
79+
}

0 commit comments

Comments
 (0)