Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 669b7f1

Browse filesBrowse files
committed
feature #35992 [Mailer] Use AsyncAws to handle SES requests (jderusse)
This PR was squashed before being merged into the 5.1-dev branch. Discussion ---------- [Mailer] Use AsyncAws to handle SES requests | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | #33183, #35468 and #35037 | License | MIT | Doc PR | TODO alternative to #33326 This PR replace the native code to call AWS SES by the new [AsyncAws](https://github.com/async-aws/aws) project maintained by @Nyholm and me. This removes complexity of signing request, and adds new features likes: - authentication via .aws/config.ini, Instance profile, WebIdentity (K8S service account) - usesignature V4 (the one recommanded by the Official SDK ) - fully compatible with API (uses the official AWS SDK interface contract to generate classes) Because it's based on `symfony/http-client`, it's fully integrable with Symfony application. Commits ------- 2124387 [Mailer] Use AsyncAws to handle SES requests
2 parents 09f9079 + 2124387 commit 669b7f1
Copy full SHA for 669b7f1

15 files changed

+502
-24
lines changed

‎UPGRADE-5.1.md

Copy file name to clipboardExpand all lines: UPGRADE-5.1.md
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ Mailer
7272
------
7373

7474
* Deprecated passing Mailgun headers without their "h:" prefix.
75+
* Deprecated the `SesApiTransport` class. It has been replaced by SesApiAsyncAwsTransport Run `composer require async-aws/ses` to use the new classes.
76+
* Deprecated the `SesHttpTransport` class. It has been replaced by SesHttpAsyncAwsTransport Run `composer require async-aws/ses` to use the new classes.
7577

7678
Messenger
7779
---------

‎UPGRADE-6.0.md

Copy file name to clipboardExpand all lines: UPGRADE-6.0.md
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ HttpKernel
6464
* Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+
6565
* Removed support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead.
6666

67+
68+
Mailer
69+
------
70+
71+
* Removed the `SesApiTransport` class. Use `SesApiAsyncAwsTransport` instead.
72+
* Removed the `SesHttpTransport` class. Use `SesHttpAsyncAwsTransport` instead.
73+
6774
Messenger
6875
---------
6976

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"require-dev": {
105105
"amphp/http-client": "^4.2",
106106
"amphp/http-tunnel": "^1.0",
107+
"async-aws/ses": "^1.0",
107108
"cache/integration-tests": "dev-master",
108109
"doctrine/annotations": "~1.0",
109110
"doctrine/cache": "~1.6",

‎src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.1.0
5+
-----
6+
7+
* Added `async-aws/ses` to communicate with AWS API.
8+
49
4.4.0
510
-----
611

+120Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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\Amazon\Tests\Transport;
13+
14+
use AsyncAws\Core\Configuration;
15+
use AsyncAws\Core\Credentials\NullProvider;
16+
use AsyncAws\Ses\SesClient;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\HttpClient\MockHttpClient;
19+
use Symfony\Component\HttpClient\Response\MockResponse;
20+
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiAsyncAwsTransport;
21+
use Symfony\Component\Mailer\Exception\HttpTransportException;
22+
use Symfony\Component\Mime\Address;
23+
use Symfony\Component\Mime\Email;
24+
use Symfony\Contracts\HttpClient\ResponseInterface;
25+
26+
class SesApiAsyncAwsTransportTest extends TestCase
27+
{
28+
/**
29+
* @dataProvider getTransportData
30+
*/
31+
public function testToString(SesApiAsyncAwsTransport $transport, string $expected)
32+
{
33+
$this->assertSame($expected, (string) $transport);
34+
}
35+
36+
public function getTransportData()
37+
{
38+
return [
39+
[
40+
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY']))),
41+
'ses+api://ACCESS_KEY@us-east-1',
42+
],
43+
[
44+
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1']))),
45+
'ses+api://ACCESS_KEY@us-west-1',
46+
],
47+
[
48+
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com']))),
49+
'ses+api://ACCESS_KEY@example.com',
50+
],
51+
[
52+
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99']))),
53+
'ses+api://ACCESS_KEY@example.com:99',
54+
],
55+
];
56+
}
57+
58+
public function testSend()
59+
{
60+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
61+
$this->assertSame('POST', $method);
62+
$this->assertSame('https://email.us-east-1.amazonaws.com/v2/email/outbound-emails', $url);
63+
64+
$content = json_decode($options['body'], true);
65+
66+
$this->assertSame('Hello!', $content['Content']['Simple']['Subject']['Data']);
67+
$this->assertSame('Saif Eddin <saif.gmati@symfony.com>', $content['Destination']['ToAddresses'][0]);
68+
$this->assertSame('Fabien <fabpot@symfony.com>', $content['FromEmailAddress']);
69+
$this->assertSame('Hello There!', $content['Content']['Simple']['Body']['Text']['Data']);
70+
$this->assertSame('<b>Hello There!</b>', $content['Content']['Simple']['Body']['Html']['Data']);
71+
72+
$json = '{"MessageId": "foobar"}';
73+
74+
return new MockResponse($json, [
75+
'http_code' => 200,
76+
]);
77+
});
78+
79+
$transport = new SesApiAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
80+
81+
$mail = new Email();
82+
$mail->subject('Hello!')
83+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
84+
->from(new Address('fabpot@symfony.com', 'Fabien'))
85+
->text('Hello There!')
86+
->html('<b>Hello There!</b>');
87+
88+
$message = $transport->send($mail);
89+
90+
$this->assertSame('foobar', $message->getMessageId());
91+
}
92+
93+
public function testSendThrowsForErrorResponse()
94+
{
95+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
96+
$xml = "<SendEmailResponse xmlns=\"https://email.amazonaws.com/doc/2010-03-31/\">
97+
<Error>
98+
<Message>i'm a teapot</Message>
99+
<Code>418</Code>
100+
</Error>
101+
</SendEmailResponse>";
102+
103+
return new MockResponse($xml, [
104+
'http_code' => 418,
105+
]);
106+
});
107+
108+
$transport = new SesApiAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
109+
110+
$mail = new Email();
111+
$mail->subject('Hello!')
112+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
113+
->from(new Address('fabpot@symfony.com', 'Fabien'))
114+
->text('Hello There!');
115+
116+
$this->expectException(HttpTransportException::class);
117+
$this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).');
118+
$transport->send($mail);
119+
}
120+
}

‎src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
use Symfony\Component\Mime\Email;
2121
use Symfony\Contracts\HttpClient\ResponseInterface;
2222

23+
/**
24+
* @group legacy
25+
*/
2326
class SesApiTransportTest extends TestCase
2427
{
2528
/**
+119Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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\Amazon\Tests\Transport;
13+
14+
use AsyncAws\Core\Configuration;
15+
use AsyncAws\Core\Credentials\NullProvider;
16+
use AsyncAws\Ses\SesClient;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\HttpClient\MockHttpClient;
19+
use Symfony\Component\HttpClient\Response\MockResponse;
20+
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpAsyncAwsTransport;
21+
use Symfony\Component\Mailer\Exception\HttpTransportException;
22+
use Symfony\Component\Mime\Address;
23+
use Symfony\Component\Mime\Email;
24+
use Symfony\Contracts\HttpClient\ResponseInterface;
25+
26+
class SesHttpAsyncAwsTransportTest extends TestCase
27+
{
28+
/**
29+
* @dataProvider getTransportData
30+
*/
31+
public function testToString(SesHttpAsyncAwsTransport $transport, string $expected)
32+
{
33+
$this->assertSame($expected, (string) $transport);
34+
}
35+
36+
public function getTransportData()
37+
{
38+
return [
39+
[
40+
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY']))),
41+
'ses+https://ACCESS_KEY@us-east-1',
42+
],
43+
[
44+
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1']))),
45+
'ses+https://ACCESS_KEY@us-west-1',
46+
],
47+
[
48+
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com']))),
49+
'ses+https://ACCESS_KEY@example.com',
50+
],
51+
[
52+
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99']))),
53+
'ses+https://ACCESS_KEY@example.com:99',
54+
],
55+
];
56+
}
57+
58+
public function testSend()
59+
{
60+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
61+
$this->assertSame('POST', $method);
62+
$this->assertSame('https://email.us-east-1.amazonaws.com/v2/email/outbound-emails', $url);
63+
64+
$body = json_decode($options['body'], true);
65+
$content = base64_decode($body['Content']['Raw']['Data']);
66+
67+
$this->assertStringContainsString('Hello!', $content);
68+
$this->assertStringContainsString('Saif Eddin <saif.gmati@symfony.com>', $content);
69+
$this->assertStringContainsString('Fabien <fabpot@symfony.com>', $content);
70+
$this->assertStringContainsString('Hello There!', $content);
71+
72+
$json = '{"MessageId": "foobar"}';
73+
74+
return new MockResponse($json, [
75+
'http_code' => 200,
76+
]);
77+
});
78+
79+
$transport = new SesHttpAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
80+
81+
$mail = new Email();
82+
$mail->subject('Hello!')
83+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
84+
->from(new Address('fabpot@symfony.com', 'Fabien'))
85+
->text('Hello There!');
86+
87+
$message = $transport->send($mail);
88+
89+
$this->assertSame('foobar', $message->getMessageId());
90+
}
91+
92+
public function testSendThrowsForErrorResponse()
93+
{
94+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
95+
$xml = "<SendEmailResponse xmlns=\"https://email.amazonaws.com/doc/2010-03-31/\">
96+
<Error>
97+
<Message>i'm a teapot</Message>
98+
<Code>418</Code>
99+
</Error>
100+
</SendEmailResponse>";
101+
102+
return new MockResponse($xml, [
103+
'http_code' => 418,
104+
]);
105+
});
106+
107+
$transport = new SesHttpAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
108+
109+
$mail = new Email();
110+
$mail->subject('Hello!')
111+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
112+
->from(new Address('fabpot@symfony.com', 'Fabien'))
113+
->text('Hello There!');
114+
115+
$this->expectException(HttpTransportException::class);
116+
$this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).');
117+
$transport->send($mail);
118+
}
119+
}

‎src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpTransportTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpTransportTest.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
use Symfony\Component\Mime\Email;
2121
use Symfony\Contracts\HttpClient\ResponseInterface;
2222

23+
/**
24+
* @group legacy
25+
*/
2326
class SesHttpTransportTest extends TestCase
2427
{
2528
/**

‎src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesTransportFactoryTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesTransportFactoryTest.php
+13-13Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
namespace Symfony\Component\Mailer\Bridge\Amazon\Tests\Transport;
1313

14-
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiTransport;
15-
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpTransport;
14+
use AsyncAws\Core\Configuration;
15+
use AsyncAws\Ses\SesClient;
16+
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiAsyncAwsTransport;
17+
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpAsyncAwsTransport;
1618
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesSmtpTransport;
1719
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory;
1820
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
@@ -67,37 +69,37 @@ public function createProvider(): iterable
6769

6870
yield [
6971
new Dsn('ses+api', 'default', self::USER, self::PASSWORD),
70-
new SesApiTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
72+
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-1']), null, $client, $logger), $dispatcher, $logger),
7173
];
7274

7375
yield [
74-
new Dsn('ses+api', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
75-
new SesApiTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger),
76+
new Dsn('ses+api', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-2']),
77+
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-2']), null, $client, $logger), $dispatcher, $logger),
7678
];
7779

7880
yield [
7981
new Dsn('ses+api', 'example.com', self::USER, self::PASSWORD, 8080),
80-
(new SesApiTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger))->setHost('example.com')->setPort(8080),
82+
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-1', 'endpoint' => 'https://example.com:8080']), null, $client, $logger), $dispatcher, $logger),
8183
];
8284

8385
yield [
8486
new Dsn('ses+https', 'default', self::USER, self::PASSWORD),
85-
new SesHttpTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
87+
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-1']), null, $client, $logger), $dispatcher, $logger),
8688
];
8789

8890
yield [
8991
new Dsn('ses', 'default', self::USER, self::PASSWORD),
90-
new SesHttpTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
92+
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-1']), null, $client, $logger), $dispatcher, $logger),
9193
];
9294

9395
yield [
9496
new Dsn('ses+https', 'example.com', self::USER, self::PASSWORD, 8080),
95-
(new SesHttpTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger))->setHost('example.com')->setPort(8080),
97+
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-1', 'endpoint' => 'https://example.com:8080']), null, $client, $logger), $dispatcher, $logger),
9698
];
9799

98100
yield [
99-
new Dsn('ses+https', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
100-
new SesHttpTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger),
101+
new Dsn('ses+https', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-2']),
102+
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-2']), null, $client, $logger), $dispatcher, $logger),
101103
];
102104

103105
yield [
@@ -127,7 +129,5 @@ public function unsupportedSchemeProvider(): iterable
127129
public function incompleteDsnProvider(): iterable
128130
{
129131
yield [new Dsn('ses+smtp', 'default', self::USER)];
130-
131-
yield [new Dsn('ses+smtp', 'default', null, self::PASSWORD)];
132132
}
133133
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.