From dcd3fa0fc87767bf1d9533706623dd7fd7c50259 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 28 Jun 2024 16:52:05 +0300 Subject: [PATCH] ref --- application/Espo/Entities/EmailAddress.php | 7 + .../Crm/EntryPoints/SubscribeAgain.php | 157 +-------- .../Modules/Crm/EntryPoints/Unsubscribe.php | 155 +-------- .../Tools/MassEmail/UnsubscribeService.php | 323 ++++++++++++++++++ 4 files changed, 352 insertions(+), 290 deletions(-) create mode 100644 application/Espo/Modules/Crm/Tools/MassEmail/UnsubscribeService.php diff --git a/application/Espo/Entities/EmailAddress.php b/application/Espo/Entities/EmailAddress.php index 8cf974c9a1..27701fc945 100644 --- a/application/Espo/Entities/EmailAddress.php +++ b/application/Espo/Entities/EmailAddress.php @@ -73,4 +73,11 @@ class EmailAddress extends Entity { return $this->get('invalid'); } + + public function setOptedOut(bool $optedOut): self + { + $this->set('optOut', $optedOut); + + return $this; + } } diff --git a/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php b/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php index fa863d8e90..e486a6b871 100644 --- a/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php +++ b/application/Espo/Modules/Crm/EntryPoints/SubscribeAgain.php @@ -29,43 +29,31 @@ namespace Espo\Modules\Crm\EntryPoints; -use Espo\Core\Exceptions\Error; use Espo\Core\Utils\Client\ActionRenderer; -use Espo\Modules\Crm\Entities\Campaign; -use Espo\Modules\Crm\Entities\CampaignLogRecord; -use Espo\Modules\Crm\Entities\EmailQueueItem; -use Espo\Modules\Crm\Entities\MassEmail; -use Espo\Modules\Crm\Entities\TargetList; -use Espo\ORM\Collection; -use Espo\Repositories\EmailAddress as EmailAddressRepository; -use Espo\Modules\Crm\Tools\MassEmail\Util as MassEmailUtil; +use Espo\Modules\Crm\Tools\MassEmail\UnsubscribeService; use Espo\Core\Api\Request; use Espo\Core\Api\Response; use Espo\Core\EntryPoint\EntryPoint; use Espo\Core\EntryPoint\Traits\NoAuth; use Espo\Core\Exceptions\BadRequest; use Espo\Core\Exceptions\NotFound; -use Espo\Core\HookManager; -use Espo\Core\ORM\EntityManager; -use Espo\Core\Utils\Hasher; use Espo\Core\Utils\Metadata; +/** + * @noinspection PhpUnused + */ class SubscribeAgain implements EntryPoint { use NoAuth; public function __construct( - private EntityManager $entityManager, - private HookManager $hookManager, private Metadata $metadata, - private Hasher $hasher, - private MassEmailUtil $util, - private ActionRenderer $actionRenderer + private ActionRenderer $actionRenderer, + private UnsubscribeService $unsubscribeService, ) {} /** * @throws BadRequest - * @throws Error * @throws NotFound */ public function run(Request $request, Response $response): void @@ -84,108 +72,15 @@ class SubscribeAgain implements EntryPoint throw new BadRequest("No id."); } - $queueItemId = $id; + $this->unsubscribeService->subscribeAgain($id); - /** @var ?EmailQueueItem $queueItem */ - $queueItem = $this->entityManager->getEntity(EmailQueueItem::ENTITY_TYPE, $queueItemId); - - if (!$queueItem) { - throw new NotFound("No item."); - } - - $campaign = null; - $target = null; - $massEmail = null; - $massEmailId = $queueItem->getMassEmailId(); - - if ($massEmailId) { - /** @var ?MassEmail $massEmail */ - $massEmail = $this->entityManager->getEntity(MassEmail::ENTITY_TYPE, $massEmailId); - } - - if ($massEmail) { - $campaignId = $massEmail->getCampaignId(); - - if ($campaignId) { - $campaign = $this->entityManager->getEntityById(Campaign::ENTITY_TYPE, $campaignId); - } - - $targetType = $queueItem->getTargetType(); - $targetId = $queueItem->getTargetId(); - - $target = $this->entityManager->getEntityById($targetType, $targetId); - - if (!$target) { - throw new NotFound("Record not found."); - } - - if ($massEmail->get('optOutEntirely')) { - $emailAddress = $target->get('emailAddress'); - - if ($emailAddress) { - $ea = $this->getEmailAddressRepository()->getByAddress($emailAddress); - - if ($ea) { - $ea->set('optOut', false); - $this->entityManager->saveEntity($ea); - } - } - } - - $link = $this->util->getLinkByEntityType($target->getEntityType()); - - /** @var Collection $targetListList */ - $targetListList = $this->entityManager - ->getRDBRepository(MassEmail::ENTITY_TYPE) - ->getRelation($massEmail, 'targetLists') - ->find(); - - foreach ($targetListList as $targetList) { - $relation = $this->entityManager - ->getRDBRepository(TargetList::ENTITY_TYPE) - ->getRelation($targetList, $link); - - if (!$relation->getColumn($target, 'optedOut')) { - continue; - } - - $relation->updateColumnsById($target->getId(), ['optedOut' => false]); - - $hookData = [ - 'link' => $link, - 'targetId' => $targetId, - 'targetType' => $targetType, - ]; - - $this->hookManager - ->process(TargetList::ENTITY_TYPE, 'afterCancelOptOut', $targetList, [], $hookData); - } - - $this->hookManager->process($target->getEntityType(), 'afterCancelOptOut', $target, [], []); - - $this->display($response, ['queueItemId' => $queueItemId]); - } - - if ($campaign && $target) { - $logRecord = $this->entityManager - ->getRDBRepository(CampaignLogRecord::ENTITY_TYPE) - ->where([ - 'queueItemId' => $queueItemId, - 'action' => CampaignLogRecord::ACTION_OPTED_OUT, - ]) - ->order('createdAt', true) - ->findOne(); - - if ($logRecord) { - $this->entityManager->removeEntity($logRecord); - } - } + $this->display($response, ['queueItemId' => $id]); } /** * @param array $actionData */ - protected function display(Response $response, array $actionData): void + private function display(Response $response, array $actionData): void { $data = [ 'actionData' => $actionData, @@ -201,43 +96,13 @@ class SubscribeAgain implements EntryPoint /** * @throws NotFound */ - protected function processWithHash(Response $response, string $emailAddress, string $hash): void + private function processWithHash(Response $response, string $emailAddress, string $hash): void { - $hash2 = $this->hasher->hash($emailAddress); - - if ($hash2 !== $hash) { - throw new NotFound(); - } - - $repository = $this->getEmailAddressRepository(); - - $ea = $repository->getByAddress($emailAddress); - - if (!$ea) { - throw new NotFound(); - } - - $entityList = $repository->getEntityListByAddressId($ea->getId()); - - if ($ea->isOptedOut()) { - $ea->set('optOut', false); - - $this->entityManager->saveEntity($ea); - - foreach ($entityList as $entity) { - $this->hookManager->process($entity->getEntityType(), 'afterCancelOptOut', $entity, [], []); - } - } + $this->unsubscribeService->subscribeAgainWithHash($emailAddress, $hash); $this->display($response, [ 'emailAddress' => $emailAddress, 'hash' => $hash, ]); } - - private function getEmailAddressRepository(): EmailAddressRepository - { - /** @var EmailAddressRepository */ - return $this->entityManager->getRepository('EmailAddress'); - } } diff --git a/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php b/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php index a3469fbfc9..32d335a6cb 100644 --- a/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php +++ b/application/Espo/Modules/Crm/EntryPoints/Unsubscribe.php @@ -29,46 +29,31 @@ namespace Espo\Modules\Crm\EntryPoints; -use Espo\Core\Exceptions\Error; use Espo\Core\Utils\Client\ActionRenderer; -use Espo\Entities\EmailAddress; -use Espo\Modules\Crm\Entities\Campaign; -use Espo\Modules\Crm\Entities\EmailQueueItem; -use Espo\Modules\Crm\Entities\MassEmail; -use Espo\Modules\Crm\Entities\TargetList; -use Espo\Modules\Crm\Tools\Campaign\LogService; -use Espo\ORM\Collection; -use Espo\Repositories\EmailAddress as EmailAddressRepository; -use Espo\Modules\Crm\Tools\MassEmail\Util as MassEmailUtil; - +use Espo\Modules\Crm\Tools\MassEmail\UnsubscribeService; use Espo\Core\Api\Request; use Espo\Core\Api\Response; use Espo\Core\EntryPoint\EntryPoint; use Espo\Core\EntryPoint\Traits\NoAuth; use Espo\Core\Exceptions\BadRequest; use Espo\Core\Exceptions\NotFound; -use Espo\Core\HookManager; -use Espo\Core\ORM\EntityManager; -use Espo\Core\Utils\Hasher; use Espo\Core\Utils\Metadata; +/** + * @noinspection PhpUnused + */ class Unsubscribe implements EntryPoint { use NoAuth; public function __construct( - private EntityManager $entityManager, - private HookManager $hookManager, private Metadata $metadata, - private Hasher $hasher, - private LogService $service, - private MassEmailUtil $util, - private ActionRenderer $actionRenderer + private ActionRenderer $actionRenderer, + private UnsubscribeService $unsubscribeService ) {} /** * @throws BadRequest - * @throws Error * @throws NotFound */ public function run(Request $request, Response $response): void @@ -87,103 +72,15 @@ class Unsubscribe implements EntryPoint throw new BadRequest("No id."); } - $queueItemId = $id; + $this->unsubscribeService->unsubscribe($id); - /** @var ?EmailQueueItem $queueItem */ - $queueItem = $this->entityManager->getEntityById(EmailQueueItem::ENTITY_TYPE, $queueItemId); - - if (!$queueItem) { - throw new NotFound("No item."); - } - - $campaign = null; - $target = null; - $massEmail = null; - $massEmailId = $queueItem->getMassEmailId(); - - if ($massEmailId) { - /** @var MassEmail $massEmail */ - $massEmail = $this->entityManager->getEntityById(MassEmail::ENTITY_TYPE, $massEmailId); - } - - if ($massEmail) { - $campaignId = $massEmail->getCampaignId(); - - if ($campaignId) { - /** @var ?Campaign $campaign */ - $campaign = $this->entityManager->getEntityById(Campaign::ENTITY_TYPE, $campaignId); - } - - $targetType = $queueItem->getTargetType(); - $targetId = $queueItem->getTargetId(); - - $target = $this->entityManager->getEntityById($targetType, $targetId); - - if (!$target) { - throw new NotFound(); - } - - if ($massEmail->optOutEntirely()) { - $emailAddress = $target->get('emailAddress'); - - if ($emailAddress) { - $ea = $this->getEmailAddressRepository()->getByAddress($emailAddress); - - if ($ea) { - $ea->set('optOut', true); - $this->entityManager->saveEntity($ea); - } - } - } - - $link = $this->util->getLinkByEntityType($target->getEntityType()); - - /** @var Collection $targetListList */ - $targetListList = $this->entityManager - ->getRDBRepository(MassEmail::ENTITY_TYPE) - ->getRelation($massEmail, 'targetLists') - ->find(); - - foreach ($targetListList as $targetList) { - $relation = $this->entityManager - ->getRDBRepository(TargetList::ENTITY_TYPE) - ->getRelation($targetList, $link); - - if ($relation->getColumn($target, 'optedOut')) { - continue; - } - - $relation->updateColumnsById($target->getId(), ['optedOut' => true]); - - $hookData = [ - 'link' => $link, - 'targetId' => $targetId, - 'targetType' => $targetType, - ]; - - $this->hookManager->process( - TargetList::ENTITY_TYPE, - 'afterOptOut', - $targetList, - [], - $hookData - ); - } - - $this->hookManager->process($target->getEntityType(), 'afterOptOut', $target, [], []); - - $this->display($response, ['queueItemId' => $queueItemId]); - } - - if ($campaign && $target) { - $this->service->logOptedOut($campaign->getId(), $queueItem, $target); - } + $this->display($response, ['queueItemId' => $id]); } /** * @param array $actionData */ - protected function display(Response $response, array $actionData): void + private function display(Response $response, array $actionData): void { $data = [ 'actionData' => $actionData, @@ -199,43 +96,13 @@ class Unsubscribe implements EntryPoint /** * @throws NotFound */ - protected function processWithHash(Response $response, string $emailAddress, string $hash): void + private function processWithHash(Response $response, string $emailAddress, string $hash): void { - $hash2 = $this->hasher->hash($emailAddress); - - if ($hash2 !== $hash) { - throw new NotFound(); - } - - $repository = $this->getEmailAddressRepository(); - - $ea = $repository->getByAddress($emailAddress); - - if (!$ea) { - throw new NotFound(); - } - - $entityList = $repository->getEntityListByAddressId($ea->getId()); - - if (!$ea->isOptedOut()) { - $ea->set('optOut', true); - - $this->entityManager->saveEntity($ea); - - foreach ($entityList as $entity) { - $this->hookManager->process($entity->getEntityType(), 'afterOptOut', $entity, [], []); - } - } + $this->unsubscribeService->unsubscribeWithHash($emailAddress, $hash); $this->display($response,[ 'emailAddress' => $emailAddress, 'hash' => $hash, ]); } - - private function getEmailAddressRepository(): EmailAddressRepository - { - /** @var EmailAddressRepository */ - return $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE); - } } diff --git a/application/Espo/Modules/Crm/Tools/MassEmail/UnsubscribeService.php b/application/Espo/Modules/Crm/Tools/MassEmail/UnsubscribeService.php new file mode 100644 index 0000000000..ba7e7e10f9 --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/MassEmail/UnsubscribeService.php @@ -0,0 +1,323 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Modules\Crm\Tools\MassEmail; + +use Espo\Core\Exceptions\NotFound; +use Espo\Core\HookManager; +use Espo\Core\ORM\EntityManager; +use Espo\Core\Utils\Hasher; +use Espo\Entities\EmailAddress; +use Espo\Modules\Crm\Entities\Campaign; +use Espo\Modules\Crm\Entities\CampaignLogRecord; +use Espo\Modules\Crm\Entities\EmailQueueItem; +use Espo\Modules\Crm\Entities\MassEmail; +use Espo\Modules\Crm\Entities\TargetList; +use Espo\Modules\Crm\Tools\Campaign\LogService; +use Espo\Modules\Crm\Tools\MassEmail\Util as MassEmailUtil; +use Espo\ORM\Collection; +use Espo\Repositories\EmailAddress as EmailAddressRepository; + +class UnsubscribeService +{ + public function __construct( + private EntityManager $entityManager, + private HookManager $hookManager, + private LogService $service, + private MassEmailUtil $util, + private Hasher $hasher, + ) {} + + /** + * @throws NotFound + */ + public function unsubscribe(string $queueItemId): void + { + /** @var ?EmailQueueItem $queueItem */ + $queueItem = $this->entityManager->getEntityById(EmailQueueItem::ENTITY_TYPE, $queueItemId); + + if (!$queueItem) { + throw new NotFound("No item."); + } + + $campaign = null; + $massEmailId = $queueItem->getMassEmailId(); + + if (!$massEmailId) { + throw new NotFound("No Mass Email ID."); + } + + /** @var ?MassEmail $massEmail */ + $massEmail = $this->entityManager->getEntityById(MassEmail::ENTITY_TYPE, $massEmailId); + + if (!$massEmail) { + throw new NotFound("Mass Email not found."); + } + + if ($massEmail->getCampaignId()) { + /** @var ?Campaign $campaign */ + $campaign = $this->entityManager->getEntityById(Campaign::ENTITY_TYPE, $massEmail->getCampaignId()); + } + + $targetType = $queueItem->getTargetType(); + $targetId = $queueItem->getTargetId(); + + $target = $this->entityManager->getEntityById($targetType, $targetId); + + if (!$target) { + throw new NotFound(); + } + + if ($massEmail->optOutEntirely()) { + $emailAddress = $target->get('emailAddress'); + + if ($emailAddress) { + $ea = $this->getEmailAddressRepository()->getByAddress($emailAddress); + + if ($ea) { + $ea->setOptedOut(true); + $this->entityManager->saveEntity($ea); + } + } + } + + $link = $this->util->getLinkByEntityType($target->getEntityType()); + + /** @var Collection $targetListList */ + $targetListList = $this->entityManager + ->getRDBRepository(MassEmail::ENTITY_TYPE) + ->getRelation($massEmail, 'targetLists') + ->find(); + + foreach ($targetListList as $targetList) { + $relation = $this->entityManager + ->getRDBRepository(TargetList::ENTITY_TYPE) + ->getRelation($targetList, $link); + + if ($relation->getColumn($target, 'optedOut')) { + continue; + } + + $relation->updateColumnsById($target->getId(), ['optedOut' => true]); + + $hookData = [ + 'link' => $link, + 'targetId' => $targetId, + 'targetType' => $targetType, + ]; + + $this->hookManager->process( + TargetList::ENTITY_TYPE, + 'afterOptOut', + $targetList, + [], + $hookData + ); + } + + $this->hookManager->process($target->getEntityType(), 'afterOptOut', $target); + + if ($campaign) { + $this->service->logOptedOut($campaign->getId(), $queueItem, $target); + } + } + + /** + * @throws NotFound + */ + public function subscribeAgain(string $queueItemId): void + { + /** @var ?EmailQueueItem $queueItem */ + $queueItem = $this->entityManager->getEntity(EmailQueueItem::ENTITY_TYPE, $queueItemId); + + if (!$queueItem) { + throw new NotFound("No item."); + } + + $campaign = null; + $massEmailId = $queueItem->getMassEmailId(); + + if (!$massEmailId) { + throw new NotFound("No Mass Email ID."); + } + + /** @var ?MassEmail $massEmail */ + $massEmail = $this->entityManager->getEntityById(MassEmail::ENTITY_TYPE, $massEmailId); + + if (!$massEmail) { + throw new NotFound("Mass Email not found."); + } + + if ($massEmail->getCampaignId()) { + /** @var ?Campaign $campaign */ + $campaign = $this->entityManager->getEntityById(Campaign::ENTITY_TYPE, $massEmail->getCampaignId()); + } + + $targetType = $queueItem->getTargetType(); + $targetId = $queueItem->getTargetId(); + + $target = $this->entityManager->getEntityById($targetType, $targetId); + + if (!$target) { + throw new NotFound("Record not found."); + } + + if ($massEmail->optOutEntirely()) { + $emailAddress = $target->get('emailAddress'); + + if ($emailAddress) { + $ea = $this->getEmailAddressRepository()->getByAddress($emailAddress); + + if ($ea) { + $ea->setOptedOut(false); + $this->entityManager->saveEntity($ea); + } + } + } + + $link = $this->util->getLinkByEntityType($target->getEntityType()); + + /** @var Collection $targetListList */ + $targetListList = $this->entityManager + ->getRDBRepository(MassEmail::ENTITY_TYPE) + ->getRelation($massEmail, 'targetLists') + ->find(); + + foreach ($targetListList as $targetList) { + $relation = $this->entityManager + ->getRDBRepository(TargetList::ENTITY_TYPE) + ->getRelation($targetList, $link); + + if (!$relation->getColumn($target, 'optedOut')) { + continue; + } + + $relation->updateColumnsById($target->getId(), ['optedOut' => false]); + + $hookData = [ + 'link' => $link, + 'targetId' => $targetId, + 'targetType' => $targetType, + ]; + + $this->hookManager + ->process(TargetList::ENTITY_TYPE, 'afterCancelOptOut', $targetList, [], $hookData); + } + + $this->hookManager->process($target->getEntityType(), 'afterCancelOptOut', $target); + + if ($campaign) { + $logRecord = $this->entityManager + ->getRDBRepository(CampaignLogRecord::ENTITY_TYPE) + ->where([ + 'queueItemId' => $queueItemId, + 'action' => CampaignLogRecord::ACTION_OPTED_OUT, + ]) + ->order('createdAt', true) + ->findOne(); + + if ($logRecord) { + $this->entityManager->removeEntity($logRecord); + } + } + } + + + /** + * @throws NotFound + */ + public function unsubscribeWithHash(string $emailAddress, string $hash): void + { + $hash2 = $this->hasher->hash($emailAddress); + + if ($hash2 !== $hash) { + throw new NotFound(); + } + + $repository = $this->getEmailAddressRepository(); + + $address = $repository->getByAddress($emailAddress); + + if (!$address) { + throw new NotFound(); + } + + $entityList = $repository->getEntityListByAddressId($address->getId()); + + if ($address->isOptedOut()) { + return; + } + + $address->setOptedOut(true); + $this->entityManager->saveEntity($address); + + foreach ($entityList as $entity) { + $this->hookManager->process($entity->getEntityType(), 'afterOptOut', $entity); + } + } + + /** + * @throws NotFound + */ + public function subscribeAgainWithHash(string $emailAddress, string $hash): void + { + $hash2 = $this->hasher->hash($emailAddress); + + if ($hash2 !== $hash) { + throw new NotFound(); + } + + $repository = $this->getEmailAddressRepository(); + + $address = $repository->getByAddress($emailAddress); + + if (!$address) { + throw new NotFound(); + } + + $entityList = $repository->getEntityListByAddressId($address->getId()); + + if (!$address->isOptedOut()) { + return; + } + + $address->setOptedOut(false); + $this->entityManager->saveEntity($address); + + foreach ($entityList as $entity) { + $this->hookManager->process($entity->getEntityType(), 'afterCancelOptOut', $entity); + } + } + + private function getEmailAddressRepository(): EmailAddressRepository + { + /** @var EmailAddressRepository */ + return $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE); + } +}