diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md index 456b1d0bf1..0129f6ea6d 100644 --- a/UPGRADE-5.1.md +++ b/UPGRADE-5.1.md @@ -72,6 +72,8 @@ Mailer ------ * 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 --------- diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index c92e3a6312..7bb77e10d2 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -64,6 +64,13 @@ HttpKernel * 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. + +Mailer +------ + + * Removed the `SesApiTransport` class. Use `SesApiAsyncAwsTransport` instead. + * Removed the `SesHttpTransport` class. Use `SesHttpAsyncAwsTransport` instead. + Messenger --------- diff --git a/composer.json b/composer.json index 5481e5d85e..afe59f04f7 100644 --- a/composer.json +++ b/composer.json @@ -104,6 +104,7 @@ "require-dev": { "amphp/http-client": "^4.2", "amphp/http-tunnel": "^1.0", + "async-aws/ses": "^1.0", "cache/integration-tests": "dev-master", "doctrine/annotations": "~1.0", "doctrine/cache": "~1.6", diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md index 9830cadaa1..dbd098ce0f 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.1.0 +----- + + * Added `async-aws/ses` to communicate with AWS API. + 4.4.0 ----- diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php new file mode 100644 index 0000000000..8678f15d6e --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php @@ -0,0 +1,120 @@ + + * + * 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 ', $content['Destination']['ToAddresses'][0]); + $this->assertSame('Fabien ', $content['FromEmailAddress']); + $this->assertSame('Hello There!', $content['Content']['Simple']['Body']['Text']['Data']); + $this->assertSame('Hello There!', $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('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 = " + + i'm a teapot + 418 + + "; + + 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); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php index 254a1ff84e..2a4adfa418 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php @@ -20,6 +20,9 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Contracts\HttpClient\ResponseInterface; +/** + * @group legacy + */ class SesApiTransportTest extends TestCase { /** diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpAsyncAwsTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpAsyncAwsTransportTest.php new file mode 100644 index 0000000000..ff3a6e23ad --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpAsyncAwsTransportTest.php @@ -0,0 +1,119 @@ + + * + * 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 ', $content); + $this->assertStringContainsString('Fabien ', $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 = " + + i'm a teapot + 418 + + "; + + 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); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpTransportTest.php index c57d00469d..994990443d 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesHttpTransportTest.php @@ -20,6 +20,9 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Contracts\HttpClient\ResponseInterface; +/** + * @group legacy + */ class SesHttpTransportTest extends TestCase { /** diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesTransportFactoryTest.php index c5d61db11d..f2c9c87cfb 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesTransportFactoryTest.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Mailer\Bridge\Amazon\Tests\Transport; -use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiTransport; -use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpTransport; +use AsyncAws\Core\Configuration; +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\SesTransportFactory; use Symfony\Component\Mailer\Test\TransportFactoryTestCase; @@ -67,37 +69,37 @@ class SesTransportFactoryTest extends TransportFactoryTestCase yield [ 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 [ - new Dsn('ses+api', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']), - new SesApiTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger), + new Dsn('ses+api', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-2']), + new SesApiAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-2']), null, $client, $logger), $dispatcher, $logger), ]; yield [ 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 [ 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 [ 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 [ 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 [ - new Dsn('ses+https', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']), - new SesHttpTransport(self::USER, self::PASSWORD, 'eu-west-1', $client, $dispatcher, $logger), + new Dsn('ses+https', 'default', self::USER, self::PASSWORD, null, ['region' => 'eu-west-2']), + new SesHttpAsyncAwsTransport(new SesClient(Configuration::create(['accessKeyId' => self::USER, 'accessKeySecret' => self::PASSWORD, 'region' => 'eu-west-2']), null, $client, $logger), $dispatcher, $logger), ]; yield [ @@ -127,7 +129,5 @@ class SesTransportFactoryTest extends TransportFactoryTestCase public function incompleteDsnProvider(): iterable { yield [new Dsn('ses+smtp', 'default', self::USER)]; - - yield [new Dsn('ses+smtp', 'default', null, self::PASSWORD)]; } } diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiAsyncAwsTransport.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiAsyncAwsTransport.php new file mode 100644 index 0000000000..ca915ab5ab --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiAsyncAwsTransport.php @@ -0,0 +1,101 @@ + + * + * 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é + */ +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); + }); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php index 95cbdbfd98..dc174e3cd1 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php @@ -21,6 +21,8 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; 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 */ diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpAsyncAwsTransport.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpAsyncAwsTransport.php new file mode 100644 index 0000000000..59450d952c --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpAsyncAwsTransport.php @@ -0,0 +1,81 @@ + + * + * 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é + */ +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(), + ], + ], + ]); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php index f11e970f23..2b4dd651e2 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php @@ -19,6 +19,8 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; 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 */ diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesTransportFactory.php index 5977d2f376..1f49413018 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesTransportFactory.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesTransportFactory.php @@ -11,6 +11,9 @@ 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\Transport\AbstractTransportFactory; use Symfony\Component\Mailer\Transport\Dsn; @@ -18,28 +21,55 @@ use Symfony\Component\Mailer\Transport\TransportInterface; /** * @author Konstantin Myakshin + * @author Jérémy Derussé */ final class SesTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { $scheme = $dsn->getScheme(); - $user = $this->getUser($dsn); - $password = $this->getPassword($dsn); $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) { - 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()); diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json b/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json index a714477274..38594ae62f 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json @@ -17,9 +17,11 @@ ], "require": { "php": "^7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/mailer": "^4.4|^5.0" }, "require-dev": { + "async-aws/ses": "^1.0", "symfony/http-client": "^4.4|^5.0" }, "autoload": {