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
-------
21243874bc
[Mailer] Use AsyncAws to handle SES requests
This commit is contained in:
commit
669b7f132b
@ -72,6 +72,8 @@ Mailer
|
|||||||
------
|
------
|
||||||
|
|
||||||
* Deprecated passing Mailgun headers without their "h:" prefix.
|
* Deprecated passing Mailgun headers without their "h:" prefix.
|
||||||
|
* Deprecated the `SesApiTransport` class. It has been replaced by SesApiAsyncAwsTransport Run `composer require async-aws/ses` to use the new classes.
|
||||||
|
* Deprecated the `SesHttpTransport` class. It has been replaced by SesHttpAsyncAwsTransport Run `composer require async-aws/ses` to use the new classes.
|
||||||
|
|
||||||
Messenger
|
Messenger
|
||||||
---------
|
---------
|
||||||
|
@ -64,6 +64,13 @@ HttpKernel
|
|||||||
* Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+
|
* Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+
|
||||||
* Removed support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead.
|
* Removed support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead.
|
||||||
|
|
||||||
|
|
||||||
|
Mailer
|
||||||
|
------
|
||||||
|
|
||||||
|
* Removed the `SesApiTransport` class. Use `SesApiAsyncAwsTransport` instead.
|
||||||
|
* Removed the `SesHttpTransport` class. Use `SesHttpAsyncAwsTransport` instead.
|
||||||
|
|
||||||
Messenger
|
Messenger
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"amphp/http-client": "^4.2",
|
"amphp/http-client": "^4.2",
|
||||||
"amphp/http-tunnel": "^1.0",
|
"amphp/http-tunnel": "^1.0",
|
||||||
|
"async-aws/ses": "^1.0",
|
||||||
"cache/integration-tests": "dev-master",
|
"cache/integration-tests": "dev-master",
|
||||||
"doctrine/annotations": "~1.0",
|
"doctrine/annotations": "~1.0",
|
||||||
"doctrine/cache": "~1.6",
|
"doctrine/cache": "~1.6",
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
5.1.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Added `async-aws/ses` to communicate with AWS API.
|
||||||
|
|
||||||
4.4.0
|
4.4.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Mailer\Bridge\Amazon\Tests\Transport;
|
||||||
|
|
||||||
|
use AsyncAws\Core\Configuration;
|
||||||
|
use AsyncAws\Core\Credentials\NullProvider;
|
||||||
|
use AsyncAws\Ses\SesClient;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiAsyncAwsTransport;
|
||||||
|
use Symfony\Component\Mailer\Exception\HttpTransportException;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
class SesApiAsyncAwsTransportTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider getTransportData
|
||||||
|
*/
|
||||||
|
public function testToString(SesApiAsyncAwsTransport $transport, string $expected)
|
||||||
|
{
|
||||||
|
$this->assertSame($expected, (string) $transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTransportData()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY']))),
|
||||||
|
'ses+api://ACCESS_KEY@us-east-1',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1']))),
|
||||||
|
'ses+api://ACCESS_KEY@us-west-1',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com']))),
|
||||||
|
'ses+api://ACCESS_KEY@example.com',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99']))),
|
||||||
|
'ses+api://ACCESS_KEY@example.com:99',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSend()
|
||||||
|
{
|
||||||
|
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
|
||||||
|
$this->assertSame('POST', $method);
|
||||||
|
$this->assertSame('https://email.us-east-1.amazonaws.com/v2/email/outbound-emails', $url);
|
||||||
|
|
||||||
|
$content = json_decode($options['body'], true);
|
||||||
|
|
||||||
|
$this->assertSame('Hello!', $content['Content']['Simple']['Subject']['Data']);
|
||||||
|
$this->assertSame('Saif Eddin <saif.gmati@symfony.com>', $content['Destination']['ToAddresses'][0]);
|
||||||
|
$this->assertSame('Fabien <fabpot@symfony.com>', $content['FromEmailAddress']);
|
||||||
|
$this->assertSame('Hello There!', $content['Content']['Simple']['Body']['Text']['Data']);
|
||||||
|
$this->assertSame('<b>Hello There!</b>', $content['Content']['Simple']['Body']['Html']['Data']);
|
||||||
|
|
||||||
|
$json = '{"MessageId": "foobar"}';
|
||||||
|
|
||||||
|
return new MockResponse($json, [
|
||||||
|
'http_code' => 200,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$transport = new SesApiAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
|
||||||
|
|
||||||
|
$mail = new Email();
|
||||||
|
$mail->subject('Hello!')
|
||||||
|
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
|
||||||
|
->from(new Address('fabpot@symfony.com', 'Fabien'))
|
||||||
|
->text('Hello There!')
|
||||||
|
->html('<b>Hello There!</b>');
|
||||||
|
|
||||||
|
$message = $transport->send($mail);
|
||||||
|
|
||||||
|
$this->assertSame('foobar', $message->getMessageId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendThrowsForErrorResponse()
|
||||||
|
{
|
||||||
|
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
|
||||||
|
$xml = "<SendEmailResponse xmlns=\"https://email.amazonaws.com/doc/2010-03-31/\">
|
||||||
|
<Error>
|
||||||
|
<Message>i'm a teapot</Message>
|
||||||
|
<Code>418</Code>
|
||||||
|
</Error>
|
||||||
|
</SendEmailResponse>";
|
||||||
|
|
||||||
|
return new MockResponse($xml, [
|
||||||
|
'http_code' => 418,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$transport = new SesApiAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
|
||||||
|
|
||||||
|
$mail = new Email();
|
||||||
|
$mail->subject('Hello!')
|
||||||
|
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
|
||||||
|
->from(new Address('fabpot@symfony.com', 'Fabien'))
|
||||||
|
->text('Hello There!');
|
||||||
|
|
||||||
|
$this->expectException(HttpTransportException::class);
|
||||||
|
$this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).');
|
||||||
|
$transport->send($mail);
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,9 @@ use Symfony\Component\Mime\Address;
|
|||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
class SesApiTransportTest extends TestCase
|
class SesApiTransportTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Mailer\Bridge\Amazon\Tests\Transport;
|
||||||
|
|
||||||
|
use AsyncAws\Core\Configuration;
|
||||||
|
use AsyncAws\Core\Credentials\NullProvider;
|
||||||
|
use AsyncAws\Ses\SesClient;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpAsyncAwsTransport;
|
||||||
|
use Symfony\Component\Mailer\Exception\HttpTransportException;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
class SesHttpAsyncAwsTransportTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider getTransportData
|
||||||
|
*/
|
||||||
|
public function testToString(SesHttpAsyncAwsTransport $transport, string $expected)
|
||||||
|
{
|
||||||
|
$this->assertSame($expected, (string) $transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTransportData()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY']))),
|
||||||
|
'ses+https://ACCESS_KEY@us-east-1',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'region' => 'us-west-1']))),
|
||||||
|
'ses+https://ACCESS_KEY@us-west-1',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com']))),
|
||||||
|
'ses+https://ACCESS_KEY@example.com',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => 'ACCESS_KEY', 'accessKeySecret' => 'SECRET_KEY', 'endpoint' => 'https://example.com:99']))),
|
||||||
|
'ses+https://ACCESS_KEY@example.com:99',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSend()
|
||||||
|
{
|
||||||
|
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
|
||||||
|
$this->assertSame('POST', $method);
|
||||||
|
$this->assertSame('https://email.us-east-1.amazonaws.com/v2/email/outbound-emails', $url);
|
||||||
|
|
||||||
|
$body = json_decode($options['body'], true);
|
||||||
|
$content = base64_decode($body['Content']['Raw']['Data']);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('Hello!', $content);
|
||||||
|
$this->assertStringContainsString('Saif Eddin <saif.gmati@symfony.com>', $content);
|
||||||
|
$this->assertStringContainsString('Fabien <fabpot@symfony.com>', $content);
|
||||||
|
$this->assertStringContainsString('Hello There!', $content);
|
||||||
|
|
||||||
|
$json = '{"MessageId": "foobar"}';
|
||||||
|
|
||||||
|
return new MockResponse($json, [
|
||||||
|
'http_code' => 200,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$transport = new SesHttpAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
|
||||||
|
|
||||||
|
$mail = new Email();
|
||||||
|
$mail->subject('Hello!')
|
||||||
|
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
|
||||||
|
->from(new Address('fabpot@symfony.com', 'Fabien'))
|
||||||
|
->text('Hello There!');
|
||||||
|
|
||||||
|
$message = $transport->send($mail);
|
||||||
|
|
||||||
|
$this->assertSame('foobar', $message->getMessageId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendThrowsForErrorResponse()
|
||||||
|
{
|
||||||
|
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
|
||||||
|
$xml = "<SendEmailResponse xmlns=\"https://email.amazonaws.com/doc/2010-03-31/\">
|
||||||
|
<Error>
|
||||||
|
<Message>i'm a teapot</Message>
|
||||||
|
<Code>418</Code>
|
||||||
|
</Error>
|
||||||
|
</SendEmailResponse>";
|
||||||
|
|
||||||
|
return new MockResponse($xml, [
|
||||||
|
'http_code' => 418,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$transport = new SesHttpAsyncAwsTransport(new SesClient(Configuration::create([]), new NullProvider(), $client));
|
||||||
|
|
||||||
|
$mail = new Email();
|
||||||
|
$mail->subject('Hello!')
|
||||||
|
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
|
||||||
|
->from(new Address('fabpot@symfony.com', 'Fabien'))
|
||||||
|
->text('Hello There!');
|
||||||
|
|
||||||
|
$this->expectException(HttpTransportException::class);
|
||||||
|
$this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).');
|
||||||
|
$transport->send($mail);
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,9 @@ use Symfony\Component\Mime\Address;
|
|||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
class SesHttpTransportTest extends TestCase
|
class SesHttpTransportTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -11,8 +11,10 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Mailer\Bridge\Amazon\Tests\Transport;
|
namespace Symfony\Component\Mailer\Bridge\Amazon\Tests\Transport;
|
||||||
|
|
||||||
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiTransport;
|
use AsyncAws\Core\Configuration;
|
||||||
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpTransport;
|
use AsyncAws\Ses\SesClient;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiAsyncAwsTransport;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpAsyncAwsTransport;
|
||||||
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesSmtpTransport;
|
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesSmtpTransport;
|
||||||
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory;
|
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory;
|
||||||
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
|
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
|
||||||
@ -67,37 +69,37 @@ class SesTransportFactoryTest extends TransportFactoryTestCase
|
|||||||
|
|
||||||
yield [
|
yield [
|
||||||
new Dsn('ses+api', 'default', self::USER, self::PASSWORD),
|
new Dsn('ses+api', 'default', self::USER, self::PASSWORD),
|
||||||
new SesApiTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
|
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-1']), null, $client, $logger), $dispatcher, $logger),
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
new Dsn('ses+api', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
|
new Dsn('ses+api', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-2']),
|
||||||
new SesApiTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger),
|
new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-2']), null, $client, $logger), $dispatcher, $logger),
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
new Dsn('ses+api', 'example.com', self::USER, self::PASSWORD, 8080),
|
new Dsn('ses+api', 'example.com', self::USER, self::PASSWORD, 8080),
|
||||||
(new SesApiTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger))->setHost('example.com')->setPort(8080),
|
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),
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
new Dsn('ses+https', 'default', self::USER, self::PASSWORD),
|
new Dsn('ses+https', 'default', self::USER, self::PASSWORD),
|
||||||
new SesHttpTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
|
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-1']), null, $client, $logger), $dispatcher, $logger),
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
new Dsn('ses', 'default', self::USER, self::PASSWORD),
|
new Dsn('ses', 'default', self::USER, self::PASSWORD),
|
||||||
new SesHttpTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger),
|
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-1']), null, $client, $logger), $dispatcher, $logger),
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
new Dsn('ses+https', 'example.com', self::USER, self::PASSWORD, 8080),
|
new Dsn('ses+https', 'example.com', self::USER, self::PASSWORD, 8080),
|
||||||
(new SesHttpTransport(self::USER, self::PASSWORD, null, $client, $dispatcher, $logger))->setHost('example.com')->setPort(8080),
|
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),
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
new Dsn('ses+https', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
|
new Dsn('ses+https', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-2']),
|
||||||
new SesHttpTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger),
|
new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-2']), null, $client, $logger), $dispatcher, $logger),
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
@ -127,7 +129,5 @@ class SesTransportFactoryTest extends TransportFactoryTestCase
|
|||||||
public function incompleteDsnProvider(): iterable
|
public function incompleteDsnProvider(): iterable
|
||||||
{
|
{
|
||||||
yield [new Dsn('ses+smtp', 'default', self::USER)];
|
yield [new Dsn('ses+smtp', 'default', self::USER)];
|
||||||
|
|
||||||
yield [new Dsn('ses+smtp', 'default', null, self::PASSWORD)];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Mailer\Bridge\Amazon\Transport;
|
||||||
|
|
||||||
|
use AsyncAws\Ses\Input\SendEmailRequest;
|
||||||
|
use AsyncAws\Ses\ValueObject\Content;
|
||||||
|
use Symfony\Component\Mailer\Envelope;
|
||||||
|
use Symfony\Component\Mailer\Exception\RuntimeException;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Mime\MessageConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*/
|
||||||
|
class SesApiAsyncAwsTransport extends SesHttpAsyncAwsTransport
|
||||||
|
{
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$configuration = $this->sesClient->getConfiguration();
|
||||||
|
if (!$configuration->isDefault('endpoint')) {
|
||||||
|
$endpoint = parse_url($configuration->get('endpoint'));
|
||||||
|
$host = $endpoint['host'].($endpoint['port'] ?? null ? ':'.$endpoint['port'] : '');
|
||||||
|
} else {
|
||||||
|
$host = $configuration->get('region');
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('ses+api://%s@%s', $configuration->get('accessKeyId'), $host);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRequest(SentMessage $message): SendEmailRequest
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$email = MessageConverter::toEmail($message->getOriginalMessage());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new RuntimeException(sprintf('Unable to send message with the "%s" transport: "%s".', __CLASS__, $e->getMessage()), 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email->getAttachments()) {
|
||||||
|
return parent::getRequest($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$envelope = $message->getEnvelope();
|
||||||
|
|
||||||
|
$request = [
|
||||||
|
'FromEmailAddress' => $envelope->getSender()->toString(),
|
||||||
|
'Destination' => [
|
||||||
|
'ToAddresses' => $this->stringifyAddresses($this->getRecipients($email, $envelope)),
|
||||||
|
],
|
||||||
|
'Content' => [
|
||||||
|
'Simple' => [
|
||||||
|
'Subject' => [
|
||||||
|
'Data' => $email->getSubject(),
|
||||||
|
'Charset' => 'utf-8',
|
||||||
|
],
|
||||||
|
'Body' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($emails = $email->getCc()) {
|
||||||
|
$request['Destination']['CcAddresses'] = $this->stringifyAddresses($emails);
|
||||||
|
}
|
||||||
|
if ($emails = $email->getBcc()) {
|
||||||
|
$request['Destination']['BccAddresses'] = $this->stringifyAddresses($emails);
|
||||||
|
}
|
||||||
|
if ($email->getTextBody()) {
|
||||||
|
$request['Content']['Simple']['Body']['Text'] = new Content([
|
||||||
|
'Data' => $email->getTextBody(),
|
||||||
|
'Charset' => $email->getTextCharset(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($email->getHtmlBody()) {
|
||||||
|
$request['Content']['Simple']['Body']['Html'] = new Content([
|
||||||
|
'Data' => $email->getHtmlBody(),
|
||||||
|
'Charset' => $email->getHtmlCharset(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SendEmailRequest($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRecipients(Email $email, Envelope $envelope): array
|
||||||
|
{
|
||||||
|
$emailRecipients = array_merge($email->getCc(), $email->getBcc());
|
||||||
|
|
||||||
|
return array_filter($envelope->getRecipients(), function (Address $address) use ($emailRecipients) {
|
||||||
|
return !\in_array($address, $emailRecipients, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,8 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
trigger_deprecation('symfony/amazon-mailer', '5.1', 'The "%s" class is deprecated, use "%s" instead. The Amazon transport now requires "AsyncAws". Run "composer require async-aws/ses".', SesApiTransport::class, SesApiAsyncAwsTransport::class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Kevin Verschaeve
|
* @author Kevin Verschaeve
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Mailer\Bridge\Amazon\Transport;
|
||||||
|
|
||||||
|
use AsyncAws\Core\Exception\Http\HttpException;
|
||||||
|
use AsyncAws\Ses\Input\SendEmailRequest;
|
||||||
|
use AsyncAws\Ses\SesClient;
|
||||||
|
use AsyncAws\Ses\ValueObject\Destination;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\HttpTransportException;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||||
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*/
|
||||||
|
class SesHttpAsyncAwsTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
/** @var SesClient */
|
||||||
|
protected $sesClient;
|
||||||
|
|
||||||
|
public function __construct(SesClient $sesClient, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->sesClient = $sesClient;
|
||||||
|
|
||||||
|
parent::__construct($dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$configuration = $this->sesClient->getConfiguration();
|
||||||
|
if (!$configuration->isDefault('endpoint')) {
|
||||||
|
$endpoint = parse_url($configuration->get('endpoint'));
|
||||||
|
$host = $endpoint['host'].($endpoint['port'] ?? null ? ':'.$endpoint['port'] : '');
|
||||||
|
} else {
|
||||||
|
$host = $configuration->get('region');
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('ses+https://%s@%s', $configuration->get('accessKeyId'), $host);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
$result = $this->sesClient->sendEmail($this->getRequest($message));
|
||||||
|
$response = $result->info()['response'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$message->setMessageId($result->getMessageId());
|
||||||
|
$message->appendDebug($response->getInfo('debug') ?? '');
|
||||||
|
} catch (HttpException $e) {
|
||||||
|
$exception = new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $e->getAwsMessage() ?: $e->getMessage(), $e->getAwsCode() ?: $e->getCode()), $e->getResponse(), $e->getCode(), $e);
|
||||||
|
$exception->appendDebug($e->getResponse()->getInfo('debug') ?? '');
|
||||||
|
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRequest(SentMessage $message): SendEmailRequest
|
||||||
|
{
|
||||||
|
return new SendEmailRequest([
|
||||||
|
'Destination' => $destination = new Destination([
|
||||||
|
'ToAddresses' => $this->stringifyAddresses($message->getEnvelope()->getRecipients()),
|
||||||
|
]),
|
||||||
|
'Content' => [
|
||||||
|
'Raw' => [
|
||||||
|
'Data' => $message->toString(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,8 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
trigger_deprecation('symfony/amazon-mailer', '5.1', 'The "%s" class is deprecated, use "%s" instead. The Amazon transport now requires "AsyncAws". Run "composer require async-aws/ses".', SesHttpTransport::class, SesHttpAsyncAwsTransport::class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Kevin Verschaeve
|
* @author Kevin Verschaeve
|
||||||
*/
|
*/
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Mailer\Bridge\Amazon\Transport;
|
namespace Symfony\Component\Mailer\Bridge\Amazon\Transport;
|
||||||
|
|
||||||
|
use AsyncAws\Core\Configuration;
|
||||||
|
use AsyncAws\Ses\SesClient;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
|
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
|
||||||
use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
|
use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
|
||||||
use Symfony\Component\Mailer\Transport\Dsn;
|
use Symfony\Component\Mailer\Transport\Dsn;
|
||||||
@ -18,28 +21,55 @@ use Symfony\Component\Mailer\Transport\TransportInterface;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Konstantin Myakshin <molodchick@gmail.com>
|
* @author Konstantin Myakshin <molodchick@gmail.com>
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
*/
|
*/
|
||||||
final class SesTransportFactory extends AbstractTransportFactory
|
final class SesTransportFactory extends AbstractTransportFactory
|
||||||
{
|
{
|
||||||
public function create(Dsn $dsn): TransportInterface
|
public function create(Dsn $dsn): TransportInterface
|
||||||
{
|
{
|
||||||
$scheme = $dsn->getScheme();
|
$scheme = $dsn->getScheme();
|
||||||
$user = $this->getUser($dsn);
|
|
||||||
$password = $this->getPassword($dsn);
|
|
||||||
$region = $dsn->getOption('region');
|
$region = $dsn->getOption('region');
|
||||||
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
|
|
||||||
$port = $dsn->getPort();
|
|
||||||
|
|
||||||
if ('ses+api' === $scheme) {
|
|
||||||
return (new SesApiTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('ses+https' === $scheme || 'ses' === $scheme) {
|
|
||||||
return (new SesHttpTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('ses+smtp' === $scheme || 'ses+smtps' === $scheme) {
|
if ('ses+smtp' === $scheme || 'ses+smtps' === $scheme) {
|
||||||
return new SesSmtpTransport($user, $password, $region, $this->dispatcher, $this->logger);
|
return new SesSmtpTransport($this->getUser($dsn), $this->getPassword($dsn), $region, $this->dispatcher, $this->logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists(SesClient::class)) {
|
||||||
|
if (!class_exists(HttpClient::class)) {
|
||||||
|
throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component or AsyncAws package is not installed. Try running "composer require async-aws/ses".', __CLASS__));
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger_deprecation('symfony/amazon-mailer', '5.1', 'Using the "%s" transport without AsyncAws is deprecated. Try running "composer require async-aws/ses".', $scheme, \get_called_class());
|
||||||
|
|
||||||
|
$user = $this->getUser($dsn);
|
||||||
|
$password = $this->getPassword($dsn);
|
||||||
|
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
|
||||||
|
$port = $dsn->getPort();
|
||||||
|
|
||||||
|
if ('ses+api' === $scheme) {
|
||||||
|
return (new SesApiTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port);
|
||||||
|
}
|
||||||
|
if ('ses+https' === $scheme || 'ses' === $scheme) {
|
||||||
|
return (new SesHttpTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch ($scheme) {
|
||||||
|
case 'ses+api':
|
||||||
|
$class = SesApiAsyncAwsTransport::class;
|
||||||
|
// no break
|
||||||
|
case 'ses':
|
||||||
|
case 'ses+https':
|
||||||
|
$class = $class ?? SesHttpAsyncAwsTransport::class;
|
||||||
|
$options = [
|
||||||
|
'region' => $dsn->getOption('region') ?: 'eu-west-1',
|
||||||
|
'accessKeyId' => $dsn->getUser(),
|
||||||
|
'accessKeySecret' => $dsn->getPassword(),
|
||||||
|
] + (
|
||||||
|
'default' === $dsn->getHost() ? [] : ['endpoint' => 'https://'.$dsn->getHost().($dsn->getPort() ? ':'.$dsn->getPort() : '')]
|
||||||
|
);
|
||||||
|
|
||||||
|
return new $class(new SesClient(Configuration::create($options), null, $this->client, $this->logger), $this->dispatcher, $this->logger);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnsupportedSchemeException($dsn, 'ses', $this->getSupportedSchemes());
|
throw new UnsupportedSchemeException($dsn, 'ses', $this->getSupportedSchemes());
|
||||||
|
@ -17,9 +17,11 @@
|
|||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.2.5",
|
"php": "^7.2.5",
|
||||||
|
"symfony/deprecation-contracts": "^2.1",
|
||||||
"symfony/mailer": "^4.4|^5.0"
|
"symfony/mailer": "^4.4|^5.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"async-aws/ses": "^1.0",
|
||||||
"symfony/http-client": "^4.4|^5.0"
|
"symfony/http-client": "^4.4|^5.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
Reference in New Issue
Block a user