diff --git a/inc/integrations/providers/amazon-ses/class-amazon-ses-integration.php b/inc/integrations/providers/amazon-ses/class-amazon-ses-integration.php index 44a7721dd..59d9f1bfa 100644 --- a/inc/integrations/providers/amazon-ses/class-amazon-ses-integration.php +++ b/inc/integrations/providers/amazon-ses/class-amazon-ses-integration.php @@ -28,10 +28,12 @@ class Amazon_SES_Integration extends Integration { /** * Amazon SES API endpoint base URL. * + * Includes the /v2/email/ prefix required by all SES v2 resource paths. + * * @since 2.5.0 * @var string */ - private const API_BASE = 'https://email.%s.amazonaws.com/v2/'; + private const API_BASE = 'https://email.%s.amazonaws.com/v2/email/'; /** * Constructor. @@ -103,7 +105,7 @@ public function get_signer(): AWS_Signer { * * @since 2.5.0 * - * @param string $endpoint Relative endpoint path (e.g. 'email-identities'). + * @param string $endpoint Relative endpoint path (e.g. 'identities', 'outbound-emails', 'account'). * @param string $method HTTP method. Defaults to GET. * @param array $data Request body data (will be JSON-encoded for non-GET requests). * @return array|\WP_Error Decoded response array or WP_Error on failure. diff --git a/inc/integrations/providers/amazon-ses/class-amazon-ses-transactional-email.php b/inc/integrations/providers/amazon-ses/class-amazon-ses-transactional-email.php index 8c1a3b1e8..bb4ba58da 100644 --- a/inc/integrations/providers/amazon-ses/class-amazon-ses-transactional-email.php +++ b/inc/integrations/providers/amazon-ses/class-amazon-ses-transactional-email.php @@ -288,7 +288,7 @@ public function on_domain_removed(string $domain, int $site_id): void { } $result = $this->get_ses()->ses_api_call( - 'email-identities/' . rawurlencode($domain), + 'identities/' . rawurlencode($domain), 'DELETE' ); @@ -311,7 +311,7 @@ public function on_domain_removed(string $domain, int $site_id): void { public function verify_domain(string $domain): array { $result = $this->get_ses()->ses_api_call( - 'email-identities', + 'identities', 'POST', [ 'EmailIdentity' => $domain, @@ -342,7 +342,7 @@ public function verify_domain(string $domain): array { public function get_domain_verification_status(string $domain): array { $result = $this->get_ses()->ses_api_call( - 'email-identities/' . rawurlencode($domain) + 'identities/' . rawurlencode($domain) ); if (is_wp_error($result)) { @@ -369,7 +369,7 @@ public function get_domain_verification_status(string $domain): array { public function get_domain_dns_records(string $domain): array { $result = $this->get_ses()->ses_api_call( - 'email-identities/' . rawurlencode($domain) + 'identities/' . rawurlencode($domain) ); if (is_wp_error($result)) { @@ -430,9 +430,11 @@ public function send_email(string $from, string $to, string $subject, string $bo */ public function get_sending_statistics(string $domain, string $period = '24h'): array { - // SES v2 does not expose per-domain stats directly via a simple endpoint. - // This returns account-level sending statistics as a proxy. - $result = $this->get_ses()->ses_api_call('account/sending-statistics'); + // SES v2 exposes account-level quota via GET /v2/email/account. + // The SendQuota object returns the total sent in the last 24 hours. + // Per-domain or per-period bounce/complaint breakdown requires + // BatchGetMetricData; this returns the available quota-level summary. + $result = $this->get_ses()->ses_api_call('account'); if (is_wp_error($result)) { return [ @@ -441,23 +443,15 @@ public function get_sending_statistics(string $domain, string $period = '24h'): ]; } - $stats = $result['SendingStatistics'] ?? []; + $quota = $result['SendQuota'] ?? []; - $totals = [ - 'sent' => 0, - 'delivered' => 0, + return [ + 'success' => true, + 'sent' => (int) ($quota['SentLast24Hours'] ?? 0), + 'delivered' => (int) ($quota['SentLast24Hours'] ?? 0), 'bounced' => 0, 'complaints' => 0, ]; - - foreach ($stats as $stat) { - $totals['sent'] += (int) ($stat['DeliveryAttempts'] ?? 0); - $totals['bounced'] += (int) ($stat['Bounces'] ?? 0); - $totals['complaints'] += (int) ($stat['Complaints'] ?? 0); - $totals['delivered'] += (int) ($stat['DeliveryAttempts'] ?? 0) - (int) ($stat['Bounces'] ?? 0); - } - - return array_merge(['success' => true], $totals); } /** diff --git a/tests/WP_Ultimo/Integrations/Providers/Amazon_SES/Amazon_SES_Integration_Test.php b/tests/WP_Ultimo/Integrations/Providers/Amazon_SES/Amazon_SES_Integration_Test.php index b10157080..679924f8c 100644 --- a/tests/WP_Ultimo/Integrations/Providers/Amazon_SES/Amazon_SES_Integration_Test.php +++ b/tests/WP_Ultimo/Integrations/Providers/Amazon_SES/Amazon_SES_Integration_Test.php @@ -55,6 +55,7 @@ public function test_get_api_base_includes_region(): void { $this->assertStringContainsString('us-east-1', $api_base); $this->assertStringContainsString('amazonaws.com', $api_base); + $this->assertStringContainsString('/v2/email/', $api_base); } public function test_get_api_base_uses_configured_region(): void { diff --git a/tests/WP_Ultimo/Integrations/Providers/Amazon_SES/Amazon_SES_Transactional_Email_Test.php b/tests/WP_Ultimo/Integrations/Providers/Amazon_SES/Amazon_SES_Transactional_Email_Test.php index bf4fcb038..9af6cec4f 100644 --- a/tests/WP_Ultimo/Integrations/Providers/Amazon_SES/Amazon_SES_Transactional_Email_Test.php +++ b/tests/WP_Ultimo/Integrations/Providers/Amazon_SES/Amazon_SES_Transactional_Email_Test.php @@ -137,7 +137,7 @@ public function test_verify_domain_returns_success_with_dns_records(): void { $this->integration->expects($this->once()) ->method('ses_api_call') - ->with('email-identities', 'POST', $this->anything()) + ->with('identities', 'POST', $this->anything()) ->willReturn([ 'IdentityType' => 'DOMAIN', 'VerifiedForSendingStatus' => false, @@ -171,7 +171,7 @@ public function test_get_domain_verification_status_returns_verified(): void { $this->integration->expects($this->once()) ->method('ses_api_call') - ->with('email-identities/example.com') + ->with('identities/example.com') ->willReturn([ 'VerifiedForSendingStatus' => true, 'DkimAttributes' => ['Status' => 'SUCCESS'], @@ -265,15 +265,13 @@ public function test_get_sending_statistics_returns_totals(): void { $this->integration->expects($this->once()) ->method('ses_api_call') - ->with('account/sending-statistics') + ->with('account') ->willReturn([ - 'SendingStatistics' => [ - [ - 'DeliveryAttempts' => 100, - 'Bounces' => 5, - 'Complaints' => 2, - 'Rejects' => 1, - ], + 'SendingEnabled' => true, + 'SendQuota' => [ + 'Max24HourSend' => 50000, + 'MaxSendRate' => 14, + 'SentLast24Hours' => 100, ], ]); @@ -281,15 +279,15 @@ public function test_get_sending_statistics_returns_totals(): void { $this->assertTrue($result['success']); $this->assertSame(100, $result['sent']); - $this->assertSame(5, $result['bounced']); - $this->assertSame(2, $result['complaints']); + $this->assertSame(0, $result['bounced']); + $this->assertSame(0, $result['complaints']); } public function test_on_domain_added_calls_verify_domain(): void { $this->integration->expects($this->once()) ->method('ses_api_call') - ->with('email-identities', 'POST', $this->anything()) + ->with('identities', 'POST', $this->anything()) ->willReturn([ 'DkimAttributes' => ['Tokens' => ['tok1', 'tok2', 'tok3']], ]); @@ -311,7 +309,7 @@ public function test_on_domain_removed_deletes_when_filter_enabled(): void { $this->integration->expects($this->once()) ->method('ses_api_call') - ->with('email-identities/example.com', 'DELETE') + ->with('identities/example.com', 'DELETE') ->willReturn([]); $this->module->on_domain_removed('example.com', 1);