Skip to content

Commit

Permalink
feat: add an admin notice when production access isn't enabled in ses
Browse files Browse the repository at this point in the history
  • Loading branch information
carlalexander committed Sep 6, 2024
1 parent 514679b commit 204327c
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 20 deletions.
15 changes: 2 additions & 13 deletions pluggable.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/
global $pagenow, $ymir;

if ($ymir->isSesEnabled() && function_exists('wp_mail') && !in_array($pagenow, ['plugins.php', 'update-core.php'], true)) {
if ($ymir->isEmailSendingEnabled() && function_exists('wp_mail') && !in_array($pagenow, ['plugins.php', 'update-core.php'], true)) {
add_filter('ymir_admin_notices', function ($notices) {
if ($notices instanceof Collection) {
$notices[] = [
Expand All @@ -32,18 +32,7 @@

return $notices;
});
} elseif ($ymir->isSesEnabled() && $ymir->isUsingVanityDomain()) {
add_filter('ymir_admin_notices', function ($notices) {
if ($notices instanceof Collection) {
$notices[] = [
'message' => 'Sending emails using SES is disabled because the site is using a vanity domain. To learn how to map a domain to your environment, check out <a href="https://docs.ymirapp.com/guides/domain-mapping.html">this guide</a>.',
'type' => 'warning',
];
}

return $notices;
});
} elseif ($ymir->isSesEnabled() && !$ymir->isUsingVanityDomain() && !function_exists('wp_mail')) {
} elseif ($ymir->isEmailSendingEnabled() && !$ymir->isUsingVanityDomain() && !function_exists('wp_mail')) {
/**
* Send email using the cloud provider email client.
*/
Expand Down
22 changes: 22 additions & 0 deletions src/CloudProvider/Aws/SesClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@
*/
class SesClient extends AbstractClient implements EmailClientInterface
{
/**
* {@inheritdoc}
*/
public function canSendEmails(): bool
{
$response = $this->request('get', '/v2/email/account');

if (200 !== $this->parseResponseStatusCode($response)) {
throw new \RuntimeException('Unable to get SES account details');
}

$response = json_decode($response['body'], true);

if (JSON_ERROR_NONE !== json_last_error()) {
throw new \RuntimeException('Unable to parse the SES response body');
} elseif (!isset($response['ProductionAccessEnabled'])) {
throw new \RuntimeException('Unable to get SES production access status');
}

return (bool) $response['ProductionAccessEnabled'];
}

/**
* {@inheritdoc}
*/
Expand Down
13 changes: 13 additions & 0 deletions src/Configuration/EmailConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,18 @@ public function modify(Container $container)
$container['email'] = function (Container $container) {
return new Email($container['event_manager'], $container['default_email_from'], $container['file_manager'], $container['phpmailer'], $container['blog_charset']);
};
$container['ymir_is_email_sending_enabled'] = $container->service(function () {
if (defined('YMIR_DISABLE_EMAIL_SENDING') && (bool) YMIR_DISABLE_EMAIL_SENDING) {
return false;
} elseif (false !== getenv('YMIR_DISABLE_EMAIL_SENDING') && (bool) getenv('YMIR_DISABLE_EMAIL_SENDING')) {
return false;
} elseif (false !== getenv('YMIR_DISABLE_SES') && (bool) getenv('YMIR_DISABLE_SES')) {
return false;
} elseif (defined('YMIR_DISABLE_SES') && (bool) YMIR_DISABLE_SES) {
return false;
}

return true;
});
}
}
1 change: 1 addition & 0 deletions src/Configuration/EventManagementConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function modify(Container $container)
new Subscriber\AssetsSubscriber($container['content_directory_name'], $container['site_url'], $container['assets_url'], $container['ymir_project_type'], $container['uploads_baseurl']),
new Subscriber\ContentDeliveryNetworkPageCachingSubscriber($container['cloudfront_client'], $container['rest_url'], $container['is_page_caching_disabled']),
new Subscriber\DisallowIndexingSubscriber($container['ymir_using_vanity_domain']),
new Subscriber\EmailSubscriber($container['email_client'], $container['ymir_is_email_sending_enabled'], $container['ymir_using_vanity_domain']),
new Subscriber\ImageEditorSubscriber($container['console_client'], $container['file_manager']),
new Subscriber\PluploadSubscriber($container['plugin_relative_path'], $container['rest_namespace'], $container['assets_url'], $container['plupload_error_messages']),
new Subscriber\RedirectSubscriber($container['ymir_mapped_domain_names'], $container['http_host'], $container['request_uri'], $container['is_multisite'], $container['ymir_project_type']),
Expand Down
5 changes: 5 additions & 0 deletions src/Email/EmailClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
*/
interface EmailClientInterface
{
/**
* Check if the client can send emails.
*/
public function canSendEmails(): bool;

/**
* Send the given email.
*/
Expand Down
13 changes: 6 additions & 7 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,19 @@ public function isActive(): bool
}

/**
* Checks if the plugin is loaded.
* Checks if email sending is enabled or not.
*/
public function isLoaded(): bool
public function isEmailSendingEnabled(): bool
{
return $this->loaded;
return $this->container['ymir_is_email_sending_enabled'];
}

/**
* Checks if SES is enabled.
* Checks if the plugin is loaded.
*/
public function isSesEnabled(): bool
public function isLoaded(): bool
{
return false === getenv('YMIR_DISABLE_SES')
&& (!defined('YMIR_DISABLE_SES') || !YMIR_DISABLE_SES);
return $this->loaded;
}

/**
Expand Down
89 changes: 89 additions & 0 deletions src/Subscriber/EmailSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

/*
* This file is part of Ymir WordPress plugin.
*
* (c) Carl Alexander <support@ymirapp.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Ymir\Plugin\Subscriber;

use Ymir\Plugin\Email\EmailClientInterface;
use Ymir\Plugin\EventManagement\SubscriberInterface;
use Ymir\Plugin\Support\Collection;

/**
* Subscriber that handles the email integration.
*/
class EmailSubscriber implements SubscriberInterface
{
/**
* The email client.
*
* @var EmailClientInterface
*/
private $client;

/**
* Flag whether email sending is enabled or not.
*
* @var bool
*/
private $isEmailSendingEnabled;

/**
* Flag whether the WordPress site is using a vanity domain or not.
*
* @var bool
*/
private $usingVanityDomain;

/**
* Constructor.
*/
public function __construct(EmailClientInterface $client, bool $isEmailSendingEnabled, bool $usingVanityDomain)
{
$this->client = $client;
$this->isEmailSendingEnabled = $isEmailSendingEnabled;
$this->usingVanityDomain = $usingVanityDomain;
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
'ymir_admin_notices' => 'displayAdminNotices',
];
}

/**
* Display admin notices related to email integration.
*/
public function displayAdminNotices($notices)
{
if (!$this->isEmailSendingEnabled || !$notices instanceof Collection) {
return $notices;
}

if (!$this->client->canSendEmails()) {
$notices[] = [
'message' => 'Sending emails using SES is disabled because your AWS isn\'t approved to send emails. To learn how to approve your AWS account, check out <a href="https://docs.ymirapp.com/team-resources/email.html#getting-your-aws-account-approved-for-sending-email">the documentation</a>.',
'type' => 'error',
];
} elseif ($this->usingVanityDomain) {
$notices[] = [
'message' => 'Sending emails using SES is disabled because the site is using a vanity domain. To learn how to map a domain to your environment, check out <a href="https://docs.ymirapp.com/guides/domain-mapping.html">this guide</a>.',
'type' => 'warning',
];
}

return $notices;
}
}
38 changes: 38 additions & 0 deletions tests/Integration/Aws/SesClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/*
* This file is part of Ymir WordPress plugin.
*
* (c) Carl Alexander <support@ymirapp.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Ymir\Plugin\Tests\Integration\Aws;

use Ymir\Plugin\CloudProvider\Aws\SesClient;
use Ymir\Plugin\Http\CurlClient;
use Ymir\Plugin\Tests\Unit\TestCase;

/**
* @covers \Ymir\Plugin\CloudProvider\Aws\SesClient
*/
class SesClientTest extends TestCase
{
public function testCanSendEmailsReturnsFalse()
{
$client = new SesClient(new CurlClient('test'), getenv('AWS_TEST_ACCESS_KEY_ID') ?: $_ENV['AWS_TEST_ACCESS_KEY_ID'], 'us-west-1', getenv('AWS_TEST_SECRET_ACCESS_KEY') ?: $_ENV['AWS_TEST_SECRET_ACCESS_KEY']);

$this->assertFalse($client->canSendEmails());
}

public function testCanSendEmailsReturnsTrue()
{
$client = new SesClient(new CurlClient('test'), getenv('AWS_TEST_ACCESS_KEY_ID') ?: $_ENV['AWS_TEST_ACCESS_KEY_ID'], 'us-east-1', getenv('AWS_TEST_SECRET_ACCESS_KEY') ?: $_ENV['AWS_TEST_SECRET_ACCESS_KEY']);

$this->assertTrue($client->canSendEmails());
}
}
30 changes: 30 additions & 0 deletions tests/Mock/EmailClientMockTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/*
* This file is part of Ymir WordPress plugin.
*
* (c) Carl Alexander <support@ymirapp.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Ymir\Plugin\Tests\Mock;

use PHPUnit\Framework\MockObject\MockObject;
use Ymir\Plugin\Email\EmailClientInterface;

trait EmailClientMockTrait
{
/**
* Creates a mock of a EmailClientInterface object.
*/
private function getEmailClientMock(): MockObject
{
return $this->getMockBuilder(EmailClientInterface::class)
->disableOriginalConstructor()
->getMock();
}
}
97 changes: 97 additions & 0 deletions tests/Unit/Subscriber/EmailSubscriberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

/*
* This file is part of Ymir WordPress plugin.
*
* (c) Carl Alexander <support@ymirapp.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Ymir\Plugin\Tests\Unit\Subscriber;

use Ymir\Plugin\Subscriber\EmailSubscriber;
use Ymir\Plugin\Support\Collection;
use Ymir\Plugin\Tests\Mock\EmailClientMockTrait;
use Ymir\Plugin\Tests\Unit\TestCase;

/**
* @covers \Ymir\Plugin\Subscriber\EmailSubscriber
*/
class EmailSubscriberTest extends TestCase
{
use EmailClientMockTrait;

public function testDisplayAdminNoticeAddsNoticeIfEmailClientCannotSendEmails()
{
$client = $this->getEmailClientMock();

$client->expects($this->once())
->method('canSendEmails')
->willReturn(false);

$notices = (new EmailSubscriber($client, true, false))->displayAdminNotices(new Collection());

$this->assertCount(1, $notices);
$this->assertSame('error', $notices[0]['type']);
$this->assertSame('Sending emails using SES is disabled because your AWS isn\'t approved to send emails. To learn how to approve your AWS account, check out <a href="https://docs.ymirapp.com/team-resources/email.html#getting-your-aws-account-approved-for-sending-email">the documentation</a>.', $notices[0]['message']);
}

public function testDisplayAdminNoticeAddsNoticeIfEmailClientCannotSendEmailsAndUsingVanityDomainIsTrue()
{
$client = $this->getEmailClientMock();

$client->expects($this->once())
->method('canSendEmails')
->willReturn(false);

$notices = (new EmailSubscriber($client, true, true))->displayAdminNotices(new Collection());

$this->assertCount(1, $notices);
$this->assertSame('error', $notices[0]['type']);
$this->assertSame('Sending emails using SES is disabled because your AWS isn\'t approved to send emails. To learn how to approve your AWS account, check out <a href="https://docs.ymirapp.com/team-resources/email.html#getting-your-aws-account-approved-for-sending-email">the documentation</a>.', $notices[0]['message']);
}

public function testDisplayAdminNoticeAddsNoticeIfUsingVanityDomainIsTrue()
{
$client = $this->getEmailClientMock();

$client->expects($this->once())
->method('canSendEmails')
->willReturn(true);

$notices = (new EmailSubscriber($client, true, true))->displayAdminNotices(new Collection());

$this->assertCount(1, $notices);
$this->assertSame('warning', $notices[0]['type']);
$this->assertSame('Sending emails using SES is disabled because the site is using a vanity domain. To learn how to map a domain to your environment, check out <a href="https://docs.ymirapp.com/guides/domain-mapping.html">this guide</a>.', $notices[0]['message']);
}

public function testDisplayAdminNoticeDoesNothingIfEmailSendingIsDisabled()
{
$this->assertCount(0, (new EmailSubscriber($this->getEmailClientMock(), false, true))->displayAdminNotices(new Collection()));
}

public function testDisplayAdminNoticeDoesNothingIfWeDontPassACollectionObject()
{
$this->assertNull((new EmailSubscriber($this->getEmailClientMock(), true, false))->displayAdminNotices(null));
}

public function testGetSubscribedEvents()
{
$callbacks = EmailSubscriber::getSubscribedEvents();

foreach ($callbacks as $callback) {
$this->assertTrue(method_exists(EmailSubscriber::class, is_array($callback) ? $callback[0] : $callback));
}

$subscribedEvents = [
'ymir_admin_notices' => 'displayAdminNotices',
];

$this->assertSame($subscribedEvents, $callbacks);
}
}

0 comments on commit 204327c

Please sign in to comment.