From 72e0f52af0d9b4456a305b69f97a21421450b593 Mon Sep 17 00:00:00 2001 From: Carl Alexander Date: Fri, 22 Sep 2023 14:40:28 -0400 Subject: [PATCH] feat: parse ses error message when throwing exception fixes #16 --- src/CloudProvider/Aws/SesClient.php | 21 ++- .../Unit/CloudProvider/Aws/SesClientTest.php | 136 ++++++++++++++++++ 2 files changed, 155 insertions(+), 2 deletions(-) diff --git a/src/CloudProvider/Aws/SesClient.php b/src/CloudProvider/Aws/SesClient.php index 5a621ef..4bb7220 100644 --- a/src/CloudProvider/Aws/SesClient.php +++ b/src/CloudProvider/Aws/SesClient.php @@ -30,9 +30,12 @@ public function sendEmail(Email $email) 'Action' => 'SendRawEmail', 'RawMessage.Data' => base64_encode($email->toString()), ])); + $statusCode = $this->parseResponseStatusCode($response); - if (200 !== $this->parseResponseStatusCode($response)) { - throw new \RuntimeException('Unable to send email'); + if (400 === $statusCode) { + throw new \RuntimeException(sprintf('SES API request failed: %s', $this->getErrorMessage($response['body'] ?? ''))); + } elseif (200 !== $this->parseResponseStatusCode($response)) { + throw new \RuntimeException(sprintf('SES API request failed with status code %d', $statusCode)); } } @@ -51,4 +54,18 @@ protected function getService(): string { return 'ses'; } + + /** + * Get the SES error message. + */ + private function getErrorMessage($body): string + { + $body = simplexml_load_string($body); + + if (!$body instanceof \SimpleXMLElement) { + return '[unable to parse error message]'; + } + + return (string) $body->Error->Message; + } } diff --git a/tests/Unit/CloudProvider/Aws/SesClientTest.php b/tests/Unit/CloudProvider/Aws/SesClientTest.php index 902d130..c2cc7f1 100644 --- a/tests/Unit/CloudProvider/Aws/SesClientTest.php +++ b/tests/Unit/CloudProvider/Aws/SesClientTest.php @@ -69,4 +69,140 @@ public function testSendEmail() (new SesClient($http, 'aws-key', 'us-east-1', 'aws-secret'))->sendEmail($email); } + + public function testSendEmailWithSesError() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('SES API request failed: Email address is not verified. The following identities failed the check in region US-EAST-1: WordPress '); + + $email = $this->getEmailMock(); + $email->expects($this->once()) + ->method('toString') + ->willReturn('email'); + + $http = $this->getHttpClientMock(); + $http->expects($this->once()) + ->method('request') + ->with( + $this->identicalTo('https://email.us-east-1.amazonaws.com/'), + $this->identicalTo([ + 'headers' => [ + 'host' => 'email.us-east-1.amazonaws.com', + 'x-amz-content-sha256' => '93c8df40dd7aabcd009385e2496d874342612b116f80638899066a8f6a2e72e6', + 'x-amz-date' => '20200515T181004Z', + 'authorization' => 'AWS4-HMAC-SHA256 Credential=aws-key/20200515/us-east-1/ses/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=105ad9051f2aaeb3471e626dfd368d2bccf87ea0626ace1ea0fbf459864eb62f', + ], + 'method' => 'POST', + 'timeout' => 300, + 'body' => 'Action=SendRawEmail&RawMessage.Data=ZW1haWw%3D', + ]) + ) + ->willReturn([ + 'body' => "\n \n Sender\n MessageRejected\n Email address is not verified. The following identities failed the check in region US-EAST-1: WordPress <wordpress@example.com>\n \n da8072bc-3550-4d98-81f5-28169cc53c7b\n\n", + 'response' => ['code' => 400], + ]); + + $gmdate = $this->getFunctionMock($this->getNamespace(SesClient::class), 'gmdate'); + $gmdate->expects($this->exactly(5)) + ->withConsecutive( + [$this->identicalTo('Ymd\THis\Z')], + [$this->identicalTo('Ymd')], + [$this->identicalTo('Ymd\THis\Z')], + [$this->identicalTo('Ymd')], + [$this->identicalTo('Ymd')] + ) + ->willReturnOnConsecutiveCalls('20200515T181004Z', '20200515', '20200515T181004Z', '20200515', '20200515'); + + (new SesClient($http, 'aws-key', 'us-east-1', 'aws-secret'))->sendEmail($email); + } + + public function testSendEmailWithSesErrorAndNoBody() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('SES API request failed: [unable to parse error message]'); + + $email = $this->getEmailMock(); + $email->expects($this->once()) + ->method('toString') + ->willReturn('email'); + + $http = $this->getHttpClientMock(); + $http->expects($this->once()) + ->method('request') + ->with( + $this->identicalTo('https://email.us-east-1.amazonaws.com/'), + $this->identicalTo([ + 'headers' => [ + 'host' => 'email.us-east-1.amazonaws.com', + 'x-amz-content-sha256' => '93c8df40dd7aabcd009385e2496d874342612b116f80638899066a8f6a2e72e6', + 'x-amz-date' => '20200515T181004Z', + 'authorization' => 'AWS4-HMAC-SHA256 Credential=aws-key/20200515/us-east-1/ses/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=105ad9051f2aaeb3471e626dfd368d2bccf87ea0626ace1ea0fbf459864eb62f', + ], + 'method' => 'POST', + 'timeout' => 300, + 'body' => 'Action=SendRawEmail&RawMessage.Data=ZW1haWw%3D', + ]) + ) + ->willReturn([ + 'response' => ['code' => 400], + ]); + + $gmdate = $this->getFunctionMock($this->getNamespace(SesClient::class), 'gmdate'); + $gmdate->expects($this->exactly(5)) + ->withConsecutive( + [$this->identicalTo('Ymd\THis\Z')], + [$this->identicalTo('Ymd')], + [$this->identicalTo('Ymd\THis\Z')], + [$this->identicalTo('Ymd')], + [$this->identicalTo('Ymd')] + ) + ->willReturnOnConsecutiveCalls('20200515T181004Z', '20200515', '20200515T181004Z', '20200515', '20200515'); + + (new SesClient($http, 'aws-key', 'us-east-1', 'aws-secret'))->sendEmail($email); + } + + public function testSendEmailWithWrongStatusCode() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('SES API request failed with status code 404'); + + $email = $this->getEmailMock(); + $email->expects($this->once()) + ->method('toString') + ->willReturn('email'); + + $http = $this->getHttpClientMock(); + $http->expects($this->once()) + ->method('request') + ->with( + $this->identicalTo('https://email.us-east-1.amazonaws.com/'), + $this->identicalTo([ + 'headers' => [ + 'host' => 'email.us-east-1.amazonaws.com', + 'x-amz-content-sha256' => '93c8df40dd7aabcd009385e2496d874342612b116f80638899066a8f6a2e72e6', + 'x-amz-date' => '20200515T181004Z', + 'authorization' => 'AWS4-HMAC-SHA256 Credential=aws-key/20200515/us-east-1/ses/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=105ad9051f2aaeb3471e626dfd368d2bccf87ea0626ace1ea0fbf459864eb62f', + ], + 'method' => 'POST', + 'timeout' => 300, + 'body' => 'Action=SendRawEmail&RawMessage.Data=ZW1haWw%3D', + ]) + ) + ->willReturn([ + 'response' => ['code' => 404], + ]); + + $gmdate = $this->getFunctionMock($this->getNamespace(SesClient::class), 'gmdate'); + $gmdate->expects($this->exactly(5)) + ->withConsecutive( + [$this->identicalTo('Ymd\THis\Z')], + [$this->identicalTo('Ymd')], + [$this->identicalTo('Ymd\THis\Z')], + [$this->identicalTo('Ymd')], + [$this->identicalTo('Ymd')] + ) + ->willReturnOnConsecutiveCalls('20200515T181004Z', '20200515', '20200515T181004Z', '20200515', '20200515'); + + (new SesClient($http, 'aws-key', 'us-east-1', 'aws-secret'))->sendEmail($email); + } }