From de1e6458450953fc491ee14833e6f55a708379c3 Mon Sep 17 00:00:00 2001 From: Karoly Gossler Date: Wed, 21 Aug 2019 15:17:48 +0200 Subject: [PATCH] added AWS instance credential provider for SES transport (api and http) --- .../Mailer/Bridge/Amazon/CHANGELOG.md | 6 +- .../Amazon/Credential/ApiTokenCredential.php | 54 ++++ .../Credential/InstanceCredentialProvider.php | 79 ++++++ .../Credential/UsernamePasswordCredential.php | 38 +++ .../Mailer/Bridge/Amazon/SesRequest.php | 246 ++++++++++++++++++ .../Amazon/Transport/SesApiTransport.php | 78 ++---- .../Amazon/Transport/SesHttpTransport.php | 47 ++-- .../Amazon/Transport/SesSmtpTransport.php | 10 +- .../Amazon/Transport/SesTransportFactory.php | 31 ++- 9 files changed, 495 insertions(+), 94 deletions(-) create mode 100644 src/Symfony/Component/Mailer/Bridge/Amazon/Credential/ApiTokenCredential.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Amazon/Credential/InstanceCredentialProvider.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Amazon/Credential/UsernamePasswordCredential.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Amazon/SesRequest.php diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md index 9830cadaa10c8..fe8c53365d84a 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md @@ -5,9 +5,13 @@ CHANGELOG ----- * [BC BREAK] Renamed and moved `Symfony\Component\Mailer\Bridge\Amazon\Http\Api\SesTransport` - to `Symfony\Component\Mailer\Bridge\Amazon\Transpor\SesApiTransport`, `Symfony\Component\Mailer\Bridge\Amazon\Http\SesTransport` + to `Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiTransport`, `Symfony\Component\Mailer\Bridge\Amazon\Http\SesTransport` to `Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpTransport`, `Symfony\Component\Mailer\Bridge\Amazon\Smtp\SesTransport` to `Symfony\Component\Mailer\Bridge\Amazon\Transport\SesSmtpTransport`. + * [BC BREAK] changed `Symfony\Component\Mailer\Bridge\Amazon\Transport\SesApiTransport::__construct` username and password arguments to credential + * [BC BREAK] changed `Symfony\Component\Mailer\Bridge\Amazon\Transport\SesHttpTransport::__construct` username and password arguments to credential + * [BC BREAK] changed `Symfony\Component\Mailer\Bridge\Amazon\Transport\SesSmtpTransport::__construct` username and password arguments to credential + * Added Instance Profile support 4.3.0 ----- diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/ApiTokenCredential.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/ApiTokenCredential.php new file mode 100644 index 0000000000000..c346b8f5ceeb0 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/ApiTokenCredential.php @@ -0,0 +1,54 @@ + + * + * 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\Credential; + +/** + * @author Karoly Gossler + */ +final class ApiTokenCredential +{ + private $accessKey; + + private $secretKey; + + private $token; + + private $expiration; + + public function __construct(string $accessKey, string $secretKey, string $token, \DateTime $expiration) + { + $this->accessKey = $accessKey; + $this->secretKey = $secretKey; + $this->token = $token; + $this->expiration = $expiration; + } + + public function getAccessKey(): string + { + return $this->accessKey; + } + + public function getSecretKey(): string + { + return $this->secretKey; + } + + public function getToken(): string + { + return $this->token; + } + + public function getExpiration(): \DateTime + { + return $this->expiration; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/InstanceCredentialProvider.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/InstanceCredentialProvider.php new file mode 100644 index 0000000000000..e2e4160e8d4b6 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/InstanceCredentialProvider.php @@ -0,0 +1,79 @@ + + * + * 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\Credential; + +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\Mailer\Exception\RuntimeException; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * Based on: aws-sdk-php / Credentials/InstanceProfileProvider.php. + * + * @author Karoly Gossler + */ +class InstanceCredentialProvider +{ + const SERVER_URI_TEMPLATE = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/%role_name%'; + + public function __construct(HttpClientInterface $client = null, int $retries = 3) + { + $this->retries = $retries; + $this->client = $client; + + if (null === $this->client) { + if (!class_exists(HttpClient::class)) { + throw new LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); + } + + $this->client = HttpClient::create(); + } + } + + public function getCredential(string $roleName): ApiTokenCredential + { + $attempts = 0; + + $instanceMetadataServerURL = str_replace('%role_name%', $roleName, self::SERVER_URI_TEMPLATE); + + while (true) { + try { + ++$attempts; + + $response = $this->client->request('GET', $instanceMetadataServerURL); + + if (200 === $response->getStatusCode()) { + $content = json_decode($response->getContent(), true); + + if (null === $content) { + throw new RuntimeException('Unexpected instance metadata response.'); + } + + if ('Success' !== $content['Code']) { + $msg = sprintf('Unexpected instance profile response: %s', $content['Code']); + throw new RuntimeException($msg); + } + + return new ApiTokenCredential($content['AccessKeyId'], $content['SecretAccessKey'], $content['Token'], new \DateTime($content['Expiration'])); + } elseif (404 === $response->getStatusCode()) { + $attempts = $this->retries + 1; + } + + sleep(pow(1.2, $attempts)); + } catch (\Exception $e) { + } + + if ($attempts > $this->retries) { + throw new RuntimeException('Error retrieving credentials from instance metadata server.'); + } + } + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/UsernamePasswordCredential.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/UsernamePasswordCredential.php new file mode 100644 index 0000000000000..0b06609d6a856 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Credential/UsernamePasswordCredential.php @@ -0,0 +1,38 @@ + + * + * 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\Credential; + +/** + * @author Karoly Gossler + */ +final class UsernamePasswordCredential +{ + private $username; + + private $password; + + public function __construct(string $username, string $password) + { + $this->username = $username; + $this->password = $password; + } + + public function getUsername(): string + { + return $this->username; + } + + public function getPassword(): string + { + return $this->password; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/SesRequest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/SesRequest.php new file mode 100644 index 0000000000000..a2c5076d88002 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/SesRequest.php @@ -0,0 +1,246 @@ + + * + * 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; + +use Symfony\Component\Mailer\Bridge\Amazon\Credential\ApiTokenCredential; +use Symfony\Component\Mailer\Bridge\Amazon\Credential\UsernamePasswordCredential; +use Symfony\Component\Mailer\Exception\RuntimeException; +use Symfony\Component\Mailer\SmtpEnvelope; +use Symfony\Component\Mime\Email; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Karoly Gossler + */ +class SesRequest +{ + private const SERVICE_NAME = 'ses'; + private const ENDPOINT_HOST = 'email.%region%.amazonaws.com'; + public const REQUEST_MODE_API = 1; + public const REQUEST_MODE_HTTP = 2; + + private $mode = self::REQUEST_MODE_API; + + private $now; + private $action; + private $region; + private $credential; + + private $requestHeaders = []; + private $canonicalHeaders = ''; + private $signedHeaders = []; + + public function __construct(HttpClientInterface $client, string $region) + { + $this->client = $client; + $this->region = $region ?: 'eu-west-1'; + } + + public function getRegion(): string + { + return $this->region; + } + + public function setRegion(string $region): self + { + $this->region = $region; + + return $this; + } + + public function getMode(): int + { + return $this->mode; + } + + public function setMode(int $mode): self + { + $this->mode = $mode; + + return $this; + } + + public function getCredential() + { + return $this->credential; + } + + public function setCredential($credential): self + { + $this->credential = $credential; + + return $this; + } + + public function sendEmail(Email $email, SmtpEnvelope $envelope): ResponseInterface + { + $this->now = new \DateTime(); + $this->action = 'SendEmail'; + $this->method = 'POST'; + + $payload = [ + 'Action' => 'SendEmail', + 'Destination.ToAddresses.member' => $this->stringifyAddresses($this->getRecipients($email, $envelope)), + 'Message.Subject.Data' => $email->getSubject(), + 'Source' => $envelope->getSender()->toString(), + ]; + + if ($emails = $email->getCc()) { + $payload['Destination.CcAddresses.member'] = $this->stringifyAddresses($emails); + } + if ($emails = $email->getBcc()) { + $payload['Destination.BccAddresses.member'] = $this->stringifyAddresses($emails); + } + if ($email->getTextBody()) { + $payload['Message.Body.Text.Data'] = $email->getTextBody(); + } + if ($email->getHtmlBody()) { + $payload['Message.Body.Html.Data'] = $email->getHtmlBody(); + } + + $this->payload = $payload; + + $this->prepareRequestHeaders(); + + return $this->sendRequest(); + } + + public function sendRawEmail(string $rawEmail): ResponseInterface + { + $this->now = new \DateTime(); + $this->action = 'SendRawEmail'; + $this->method = 'POST'; + $this->payload = [ + 'Action' => 'SendRawEmail', + 'RawMessage.Data' => base64_encode($rawEmail), + ]; + + $this->prepareRequestHeaders(); + + return $this->sendRequest(); + } + + private function sendRequest(): ResponseInterface + { + $options = [ + 'headers' => $this->requestHeaders, + 'body' => $this->payload, + ]; + + return $this->client->request($this->method, 'https://'.$this->getEndpointHost(), $options); + } + + private function prepareRequestHeaders(): void + { + $this->requestHeaders = []; + + if (self::REQUEST_MODE_API === $this->mode) { + $this->requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + $this->requestHeaders['Host'] = $this->getEndpointHost(); + $this->requestHeaders['X-Amz-Date'] = $this->now->format('Ymd\THis\Z'); + if ($this->credential instanceof ApiTokenCredential) { + $this->requestHeaders['X-Amz-Security-Token'] = $this->credential->getToken(); + } + ksort($this->requestHeaders); + + $canonicalHeadersBuffer = []; + foreach ($this->requestHeaders as $key => $value) { + $canonicalHeadersBuffer[] = strtolower($key).':'.$value; + } + $canonicalHeaders = implode("\n", $canonicalHeadersBuffer); + + $signedHeadersBuffer = []; + foreach ($this->requestHeaders as $key => $value) { + $signedHeadersBuffer[] = strtolower($key); + } + $signedHeaders = implode(';', $signedHeadersBuffer); + + $hashedCanonicalRequest = hash('sha256', sprintf( + "%s\n/\n\n%s\n\n%s\n%s", + $this->method, + $canonicalHeaders, + $signedHeaders, + hash('sha256', $this->arrayToSignableString($this->payload)), + )); + + $scope = $this->now->format('Ymd').'/'.$this->region.'/'.self::SERVICE_NAME.'/aws4_request'; + + $stringToSign = sprintf( + "%s\n%s\n%s\n%s", + 'AWS4-HMAC-SHA256', + $this->now->format('Ymd\THis\Z'), + $scope, + $hashedCanonicalRequest, + ); + + if ($this->credential instanceof ApiTokenCredential) { + $keySecret = 'AWS4'.$this->credential->getSecretKey(); + } elseif ($this->credential instanceof UsernamePasswordCredential) { + $keySecret = 'AWS4'.$this->credential->getPassword(); + } else { + throw new RuntimeException('Unsupported credential'); + } + + $keyDate = hash_hmac('sha256', $this->now->format('Ymd'), $keySecret, true); + $keyRegion = hash_hmac('sha256', $this->region, $keyDate, true); + $keyService = hash_hmac('sha256', self::SERVICE_NAME, $keyRegion, true); + $keySigning = hash_hmac('sha256', 'aws4_request', $keyService, true); + $signature = hash_hmac('sha256', $stringToSign, $keySigning); + + if ($this->credential instanceof ApiTokenCredential) { + $fullCredentialString = $this->credential->getAccessKey().'/'.$scope; + } elseif ($this->credential instanceof UsernamePasswordCredential) { + $fullCredentialString = $this->credential->getUsername().'/'.$scope; + } + + $this->requestHeaders['Authorization'] = sprintf( + 'AWS4-HMAC-SHA256 Credential=%s, SignedHeaders=%s, Signature=%s', + $fullCredentialString, + $signedHeaders, + $signature + ); + } + + private function arrayToSignableString(array $array): string + { + $buffer = ''; + + foreach ($array as $key => $value) { + $key = str_replace('%7E', '~', rawurlencode($key)); + $value = str_replace('%7E', '~', rawurlencode($value)); + + $buffer .= '&'.$key.'='.$value; + } + + return substr($buffer, 1); + } + + private function getEndpointHost(): string + { + return str_replace('%region%', $this->region, self::ENDPOINT_HOST); + } + + /** + * @param Address[] $addresses + * + * @return string[] + */ + private function stringifyAddresses(array $addresses): array + { + return array_map(function (Address $a) { + return $a->toString(); + }, $addresses); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php index 5305540b39860..5ee004eac51eb 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php @@ -25,19 +25,16 @@ */ class SesApiTransport extends AbstractApiTransport { - private const ENDPOINT = 'https://email.%region%.amazonaws.com'; - - private $accessKey; - private $secretKey; + private $credential; private $region; /** - * @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1) + * @param ApiTokenCredential|UsernamePasswordCredential $credential credential object for SES authentication. ApiTokenCredential and UsernamePasswordCredential are supported. + * @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1) */ - public function __construct(string $accessKey, string $secretKey, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + public function __construct($credential, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { - $this->accessKey = $accessKey; - $this->secretKey = $secretKey; + $this->credential = $credential; $this->region = $region ?: 'eu-west-1'; parent::__construct($client, $dispatcher, $logger); @@ -45,23 +42,27 @@ public function __construct(string $accessKey, string $secretKey, string $region public function getName(): string { - return sprintf('api://%s@ses?region=%s', $this->accessKey, $this->region); + $login = null; + if ($this->credential instanceof ApiTokenCredential) { + $login = $this->credential->getAccessKey(); + } else { + $login = $this->credential->getUsername(); + } + + return sprintf('api://%s@ses?region=%s', $login, $this->region); } protected function doSendApi(Email $email, SmtpEnvelope $envelope): ResponseInterface { - $date = gmdate('D, d M Y H:i:s e'); - $auth = sprintf('AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA256,Signature=%s', $this->accessKey, $this->getSignature($date)); + $request = new SesRequest($this->client, $this->region); + $request->setMode(SesRequest::REQUEST_MODE_API); + $request->setCredential($this->credential); - $endpoint = str_replace('%region%', $this->region, self::ENDPOINT); - $response = $this->client->request('POST', $endpoint, [ - 'headers' => [ - 'X-Amzn-Authorization' => $auth, - 'Date' => $date, - 'Content-Type' => 'application/x-www-form-urlencoded', - ], - 'body' => $this->getPayload($email, $envelope), - ]); + if ($email->getAttachments()) { + $response = $request->sendRawEmail($email->toString()); + } else { + $response = $request->sendEmail($email, $envelope); + } if (200 !== $response->getStatusCode()) { $error = new \SimpleXMLElement($response->getContent(false)); @@ -71,41 +72,4 @@ protected function doSendApi(Email $email, SmtpEnvelope $envelope): ResponseInte return $response; } - - private function getSignature(string $string): string - { - return base64_encode(hash_hmac('sha256', $string, $this->secretKey, true)); - } - - private function getPayload(Email $email, SmtpEnvelope $envelope): array - { - if ($email->getAttachments()) { - return [ - 'Action' => 'SendRawEmail', - 'RawMessage.Data' => base64_encode($email->toString()), - ]; - } - - $payload = [ - 'Action' => 'SendEmail', - 'Destination.ToAddresses.member' => $this->stringifyAddresses($this->getRecipients($email, $envelope)), - 'Message.Subject.Data' => $email->getSubject(), - 'Source' => $envelope->getSender()->toString(), - ]; - - if ($emails = $email->getCc()) { - $payload['Destination.CcAddresses.member'] = $this->stringifyAddresses($emails); - } - if ($emails = $email->getBcc()) { - $payload['Destination.BccAddresses.member'] = $this->stringifyAddresses($emails); - } - if ($email->getTextBody()) { - $payload['Message.Body.Text.Data'] = $email->getTextBody(); - } - if ($email->getHtmlBody()) { - $payload['Message.Body.Html.Data'] = $email->getHtmlBody(); - } - - return $payload; - } } diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php index e93511fdde8a8..aab945ccc670c 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php @@ -12,6 +12,9 @@ namespace Symfony\Component\Mailer\Bridge\Amazon\Transport; use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Bridge\Amazon\Credential\ApiTokenCredential; +use Symfony\Component\Mailer\Bridge\Amazon\Credential\UsernamePasswordCredential; +use Symfony\Component\Mailer\Bridge\Amazon\SesRequest; use Symfony\Component\Mailer\Exception\HttpTransportException; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractHttpTransport; @@ -24,19 +27,16 @@ */ class SesHttpTransport extends AbstractHttpTransport { - private const ENDPOINT = 'https://email.%region%.amazonaws.com'; - - private $accessKey; - private $secretKey; + private $credential; private $region; /** - * @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1) + * @param ApiTokenCredential|UsernamePasswordCredential $credential credential object for SES authentication. ApiTokenCredential and UsernamePasswordCredential are supported. + * @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1) */ - public function __construct(string $accessKey, string $secretKey, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + public function __construct($credential, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { - $this->accessKey = $accessKey; - $this->secretKey = $secretKey; + $this->credential = $credential; $this->region = $region ?: 'eu-west-1'; parent::__construct($client, $dispatcher, $logger); @@ -44,25 +44,23 @@ public function __construct(string $accessKey, string $secretKey, string $region public function getName(): string { - return sprintf('http://%s@ses?region=%s', $this->accessKey, $this->region); + $login = null; + if ($this->credential instanceof ApiTokenCredential) { + $login = $this->credential->getAccessKey(); + } else { + $login = $this->credential->getUsername(); + } + + return sprintf('http://%s@ses?region=%s', $login, $this->region); } protected function doSendHttp(SentMessage $message): ResponseInterface { - $date = gmdate('D, d M Y H:i:s e'); - $auth = sprintf('AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA256,Signature=%s', $this->accessKey, $this->getSignature($date)); + $request = new SesRequest($this->client, $this->region); + $request->setMode(SesRequest::REQUEST_MODE_HTTP); + $request->setCredential($this->credential); - $endpoint = str_replace('%region%', $this->region, self::ENDPOINT); - $response = $this->client->request('POST', $endpoint, [ - 'headers' => [ - 'X-Amzn-Authorization' => $auth, - 'Date' => $date, - ], - 'body' => [ - 'Action' => 'SendRawEmail', - 'RawMessage.Data' => base64_encode($message->toString()), - ], - ]); + $response = $request->sendRawEmail($message->toString()); if (200 !== $response->getStatusCode()) { $error = new \SimpleXMLElement($response->getContent(false)); @@ -72,9 +70,4 @@ protected function doSendHttp(SentMessage $message): ResponseInterface return $response; } - - private function getSignature(string $string): string - { - return base64_encode(hash_hmac('sha256', $string, $this->secretKey, true)); - } } diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesSmtpTransport.php index 2c53200689514..18f2b137d1c65 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesSmtpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesSmtpTransport.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Mailer\Bridge\Amazon\Transport; use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Bridge\Amazon\Credential\UsernamePasswordCredential; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -21,13 +22,14 @@ class SesSmtpTransport extends EsmtpTransport { /** - * @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1) + * @param UsernamePasswordCredential $credential credential object for SES authentication + * @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1) */ - public function __construct(string $username, string $password, string $region = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + public function __construct(UsernamePasswordCredential $credential, string $region = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { parent::__construct(sprintf('email-smtp.%s.amazonaws.com', $region ?: 'eu-west-1'), 587, true, $dispatcher, $logger); - $this->setUsername($username); - $this->setPassword($password); + $this->setUsername($credential->getUsername()); + $this->setPassword($credential->getPassword()); } } diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesTransportFactory.php index 0dba1d998b465..d2051313e9598 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 Symfony\Component\Mailer\Bridge\Amazon\Credential\InstanceCredentialProvider; +use Symfony\Component\Mailer\Bridge\Amazon\Credential\UsernamePasswordCredential; +use Symfony\Component\Mailer\Exception\IncompleteDsnException; use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; use Symfony\Component\Mailer\Transport\AbstractTransportFactory; use Symfony\Component\Mailer\Transport\Dsn; @@ -21,23 +24,41 @@ */ final class SesTransportFactory extends AbstractTransportFactory { + private $credentialProvider; + + public function __construct(EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null) + { + parent::__construct($dispatcher, $client, $logger); + + $this->credentialProvider = new InstanceCredentialProvider($client); + } + public function create(Dsn $dsn): TransportInterface { $scheme = $dsn->getScheme(); - $user = $this->getUser($dsn); - $password = $this->getPassword($dsn); + try { + $credential = new UsernamePasswordCredential($this->getUser($dsn), $this->getPassword($dsn)); + } catch (IncompleteDsnException $e) { + $role = $dsn->getOption('role'); + + if (null === $role) { + throw new IncompleteDsnException('User and password nor role is not set.'); + } + + $credential = $this->credentialProvider->getCredential($role); + } $region = $dsn->getOption('region'); if ('api' === $scheme) { - return new SesApiTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger); + return new SesApiTransport($credential, $region, $this->client, $this->dispatcher, $this->logger); } if ('http' === $scheme) { - return new SesHttpTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger); + return new SesHttpTransport($credential, $region, $this->client, $this->dispatcher, $this->logger); } if ('smtp' === $scheme || 'smtps' === $scheme) { - return new SesSmtpTransport($user, $password, $region, $this->dispatcher, $this->logger); + return new SesSmtpTransport($credential, $region, $this->dispatcher, $this->logger); } throw new UnsupportedSchemeException($dsn, ['api', 'http', 'smtp', 'smtps']);