Skip to content

[9.x] Re-implement original SesTransport #40696

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"ext-posix": "Required to use all features of the queue worker.",
"ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).",
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
"aws/aws-sdk-php": "Required to use the SQS queue driver and DynamoDb failed job storage (^3.198.1).",
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.198.1).",
"brianium/paratest": "Required to run tests in parallel (^6.0).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.13.3|^3.1.4).",
"filp/whoops": "Required for friendly error pages in development (^2.14.3).",
Expand All @@ -157,7 +157,6 @@
"predis/predis": "Required to use the predis connector (^1.1.9).",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).",
"symfony/amazon-mailer": "Required to enable support for the SES mail transport (^6.0).",
"symfony/cache": "Required to PSR-6 cache bridge (^6.0).",
"symfony/filesystem": "Required to enable support for relative symbolic links (^6.0).",
"symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.0).",
Expand Down
30 changes: 18 additions & 12 deletions src/Illuminate/Mail/MailManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

namespace Illuminate\Mail;

use Aws\Ses\SesClient;
use Closure;
use Illuminate\Contracts\Mail\Factory as FactoryContract;
use Illuminate\Log\LogManager;
use Illuminate\Mail\Transport\ArrayTransport;
use Illuminate\Mail\Transport\LogTransport;
use Illuminate\Mail\Transport\SesTransport;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
use Symfony\Component\Mailer\Transport\Dsn;
Expand Down Expand Up @@ -234,20 +235,25 @@ protected function createSesTransport(array $config)

$config = Arr::except($config, ['transport']);

$factory = new SesTransportFactory();
return new SesTransport(
new SesClient($this->addSesCredentials($config)),
$config['options'] ?? []
);
}

if (! isset($config['session_token']) && isset($config['token'])) {
$config['session_token'] = $config['token'];
/**
* Add the SES credentials to the configuration array.
*
* @param array $config
* @return array
*/
protected function addSesCredentials(array $config)
{
if (! empty($config['key']) && ! empty($config['secret'])) {
$config['credentials'] = Arr::only($config, ['key', 'secret', 'token']);
}

return $factory->create(new Dsn(
'ses+api',
'default',
$config['key'] ?? null,
$config['secret'] ?? null,
$config['port'] ?? null,
$config
));
return $config;
}

/**
Expand Down
2 changes: 0 additions & 2 deletions src/Illuminate/Mail/Transport/ArrayTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ public function __construct()

/**
* {@inheritdoc}
*
* @return int
*/
public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage
{
Expand Down
2 changes: 0 additions & 2 deletions src/Illuminate/Mail/Transport/LogTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ public function __construct(LoggerInterface $logger)

/**
* {@inheritdoc}
*
* @return int
*/
public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage
{
Expand Down
104 changes: 104 additions & 0 deletions src/Illuminate/Mail/Transport/SesTransport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace Illuminate\Mail\Transport;

use Aws\Exception\AwsException;
use Aws\Ses\SesClient;
use Exception;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mailer\Transport\TransportInterface;

class SesTransport extends AbstractTransport implements TransportInterface
{
/**
* The Amazon SES instance.
*
* @var \Aws\Ses\SesClient
*/
protected $ses;

/**
* The Amazon SES transmission options.
*
* @var array
*/
protected $options = [];

/**
* Create a new SES transport instance.
*
* @param \Aws\Ses\SesClient $ses
* @param array $options
* @return void
*/
public function __construct(SesClient $ses, $options = [])
{
$this->ses = $ses;
$this->options = $options;

parent::__construct();
}

/**
* {@inheritDoc}
*/
protected function doSend(SentMessage $message): void
{
try {
$this->ses->sendRawEmail(
array_merge(
$this->options, [
'Source' => $message->getEnvelope()->getSender()->toString(),
'RawMessage' => [
'Data' => $message->toString(),
],
]
)
);
} catch (AwsException $e) {
throw new Exception('Request to AWS SES API failed.', $e->getCode(), $e);
}
}

/**
* Get the string representation of the transport.
*
* @return string
*/
public function __toString(): string
{
return 'ses';
}

/**
* Get the Amazon SES client for the SesTransport instance.
*
* @return \Aws\Ses\SesClient
*/
public function ses()
{
return $this->ses;
}

/**
* Get the transmission options being used by the transport.
*
* @return array
*/
public function getOptions()
{
return $this->options;
}

/**
* Set the transmission options being used by the transport.
*
* @param array $options
* @return array
*/
public function setOptions(array $options)
{
return $this->options = $options;
}
}
2 changes: 1 addition & 1 deletion src/Illuminate/Mail/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
}
},
"suggest": {
"symfony/amazon-mailer": "Required to enable support for the SES mail transport (^6.0).",
"aws/aws-sdk-php": "Required to use the SES mail driver (^3.198.1).",
"symfony/http-client": "Required to use the Symfony API mail transports (^6.0).",
"symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.0).",
"symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.0)."
Expand Down
88 changes: 87 additions & 1 deletion tests/Mail/MailSesTransportTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,115 @@

namespace Illuminate\Tests\Mail;

use Aws\Ses\SesClient;
use Illuminate\Config\Repository;
use Illuminate\Container\Container;
use Illuminate\Mail\MailManager;
use Illuminate\Mail\Transport\SesTransport;
use Illuminate\View\Factory;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Email;

class MailSesTransportTest extends TestCase
{
protected function tearDown(): void
{
m::close();

parent::tearDown();
}

public function testGetTransport()
{
$container = new Container;

$container->singleton('config', function () {
return new Repository([
'services.ses' => [
'key' => 'foo',
'secret' => 'bar',
'region' => 'us-east-1',
],
]);
});

$manager = new MailManager($container);

/** @var \Illuminate\Mail\Transport\SesTransport $transport */
$transport = $manager->createSymfonyTransport(['transport' => 'ses']);

$this->assertSame('ses+api://random_key@us-east-1', $transport->__toString());
$ses = $transport->ses();

$this->assertSame('us-east-1', $ses->getRegion());

$this->assertSame('ses', (string) $transport);
}

public function testSend()
{
$message = new Email();
$message->subject('Foo subject');
$message->text('Bar body');
$message->sender('myself@example.com');
$message->to('me@example.com');
$message->bcc('you@example.com');

$client = m::mock(SesClient::class);
$client->shouldReceive('sendRawEmail')->once();

(new SesTransport($client))->send($message);
}

public function testSesLocalConfiguration()
{
$container = new Container;

$container->singleton('config', function () {
return new Repository([
'mail' => [
'mailers' => [
'ses' => [
'transport' => 'ses',
'region' => 'eu-west-1',
'options' => [
'ConfigurationSetName' => 'Laravel',
'Tags' => [
['Name' => 'Laravel', 'Value' => 'Framework'],
],
],
],
],
],
'services' => [
'ses' => [
'region' => 'us-east-1',
],
],
]);
});

$container->instance('view', $this->createMock(Factory::class));

$container->bind('events', function () {
return null;
});

$manager = new MailManager($container);

/** @var \Illuminate\Mail\Mailer $mailer */
$mailer = $manager->mailer('ses');

/** @var \Illuminate\Mail\Transport\SesTransport $transport */
$transport = $mailer->getSymfonyTransport();

$this->assertSame('eu-west-1', $transport->ses()->getRegion());

$this->assertSame([
'ConfigurationSetName' => 'Laravel',
'Tags' => [
['Name' => 'Laravel', 'Value' => 'Framework'],
],
], $transport->getOptions());
}
}