diff --git a/config/services/messenger.yml b/config/services/messenger.yml index 6ae953d4..38130f6e 100644 --- a/config/services/messenger.yml +++ b/config/services/messenger.yml @@ -11,6 +11,10 @@ services: resource: '../../src/Domain/Subscription/MessageHandler' tags: [ 'messenger.message_handler' ] - PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler: + PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\CampaignProcessorMessageHandler: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\TestCampaignProcessorMessageHandler: autowire: true autoconfigure: true diff --git a/config/services/resolvers.yml b/config/services/resolvers.yml index 6dfab328..baa9d3b9 100644 --- a/config/services/resolvers.yml +++ b/config/services/resolvers.yml @@ -1,4 +1,16 @@ services: + _defaults: + autowire: true + autoconfigure: true + + _instanceof: + PhpList\Core\Domain\Configuration\Service\Placeholder\PlaceholderValueResolverInterface: + tags: ['phplist.placeholder_resolver'] + PhpList\Core\Domain\Configuration\Service\Placeholder\PatternValueResolverInterface: + tags: ['phplist.pattern_resolver'] + PhpList\Core\Domain\Configuration\Service\Placeholder\SupportingPlaceholderResolverInterface: + tags: ['phplist.supporting_placeholder_resolver'] + PhpList\Core\Domain\Subscription\Service\Resolver\AttributeValueResolver: arguments: $providers: @@ -14,26 +26,5 @@ services: arguments: - !tagged_iterator { tag: 'phplist.bounce_action_handler' } - PhpList\Core\Domain\Configuration\Service\Placeholder\UnsubscribeUrlValueResolver: - autowire: true - autoconfigure: true - - PhpList\Core\Domain\Configuration\Service\Placeholder\ConfirmationUrlValueResolver: - autowire: true - autoconfigure: true - - PhpList\Core\Domain\Configuration\Service\Placeholder\PreferencesUrlValueResolver: - autowire: true - autoconfigure: true - - PhpList\Core\Domain\Configuration\Service\Placeholder\SubscribeUrlValueResolver: - autowire: true - autoconfigure: true - - _instanceof: - PhpList\Core\Domain\Configuration\Service\Placeholder\PlaceholderValueResolverInterface: - tags: ['phplist.placeholder_resolver'] - PhpList\Core\Domain\Configuration\Service\Placeholder\PatternValueResolverInterface: - tags: [ 'phplist.pattern_resolver' ] - PhpList\Core\Domain\Configuration\Service\Placeholder\SupportingPlaceholderResolverInterface: - tags: [ 'phplist.supporting_placeholder_resolver' ] + PhpList\Core\Domain\Configuration\Service\Placeholder\: + resource: '../../src/Domain/Configuration/Service/Placeholder/*' diff --git a/src/DependencyInjection/PhpListCoreExtension.php b/src/DependencyInjection/PhpListCoreExtension.php index e04ad940..a41381f0 100644 --- a/src/DependencyInjection/PhpListCoreExtension.php +++ b/src/DependencyInjection/PhpListCoreExtension.php @@ -11,12 +11,26 @@ class PhpListCoreExtension extends Extension { + private array $configFiles = [ + 'builders.yml', + 'commands.yml', + 'managers.yml', + 'mappers.yml', + 'messengers.yml', + 'processors.yml', + 'providers.yml', + 'repositories.yml', + 'resolvers.yml', + 'services.yml', + 'validators.yml', + ]; + public function load(array $configs, ContainerBuilder $container): void { $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config/services')); // Load core service definitions if present (keep optional to avoid breaking consumers) - foreach (['services.yml', 'builders.yml', 'managers.yml'] as $file) { + foreach ($this->configFiles as $file) { $path = __DIR__ . '/../../config/services/' . $file; if (is_file($path) && is_readable($path)) { $loader->load($file); diff --git a/src/Domain/Analytics/Service/LinkTrackService.php b/src/Domain/Analytics/Service/LinkTrackService.php index 60230dd3..d6d60f95 100644 --- a/src/Domain/Analytics/Service/LinkTrackService.php +++ b/src/Domain/Analytics/Service/LinkTrackService.php @@ -4,6 +4,7 @@ namespace PhpList\Core\Domain\Analytics\Service; +use Doctrine\ORM\EntityManagerInterface; use PhpList\Core\Core\ParameterProvider; use PhpList\Core\Domain\Analytics\Exception\MissingMessageIdException; use PhpList\Core\Domain\Analytics\Model\LinkTrack; @@ -12,13 +13,11 @@ class LinkTrackService { - private LinkTrackRepository $linkTrackRepository; - private ParameterProvider $paramProvider; - - public function __construct(LinkTrackRepository $linkTrackRepository, ParameterProvider $paramProvider) - { - $this->linkTrackRepository = $linkTrackRepository; - $this->paramProvider = $paramProvider; + public function __construct( + private readonly LinkTrackRepository $linkTrackRepository, + private readonly ParameterProvider $paramProvider, + private readonly EntityManagerInterface $entityManager, + ) { } public function getUrlById(int $id): ?string @@ -58,7 +57,6 @@ public function extractAndSaveLinks(MessagePrecacheDto $content, int $userId, ?i $links = array_unique($links); $savedLinks = []; - foreach ($links as $url) { $existingLinkTrack = $this->linkTrackRepository->findByUrlUserIdAndMessageId($url, $userId, $messageId); if ($existingLinkTrack !== null) { @@ -74,6 +72,8 @@ public function extractAndSaveLinks(MessagePrecacheDto $content, int $userId, ?i $savedLinks[] = $linkTrack; } + $this->entityManager->flush(); + return $savedLinks; } diff --git a/src/Domain/Common/RemotePageFetcher.php b/src/Domain/Common/RemotePageFetcher.php index 30ac316e..fdeb01db 100644 --- a/src/Domain/Common/RemotePageFetcher.php +++ b/src/Domain/Common/RemotePageFetcher.php @@ -60,13 +60,16 @@ public function __invoke(string $url, array $userData): string if (!empty($content)) { $content = $this->htmlUrlRewriter->addAbsoluteResources($content, $url); - $this->eventLogManager->log(page: 'unknown page', entry:'Fetching '.$url.' success'); + $this->eventLogManager->log(page: 'unknown page', entry: 'Fetching ' . $url . ' success'); $caches = $this->urlCacheRepository->getByUrl($url); foreach ($caches as $cache) { $this->entityManager->remove($cache); } - $urlCache = (new UrlCache())->setUrl($url)->setContent($content)->setLastModified($lastModified); + $urlCache = (new UrlCache()) + ->setUrl($url) + ->setContent($content) + ->setLastModified($lastModified); $this->urlCacheRepository->persist($urlCache); $this->cache->set($cacheKey, [ diff --git a/src/Domain/Configuration/Service/MessagePlaceholderProcessor.php b/src/Domain/Configuration/Service/MessagePlaceholderProcessor.php index ba216a1a..f0443f4a 100644 --- a/src/Domain/Configuration/Service/MessagePlaceholderProcessor.php +++ b/src/Domain/Configuration/Service/MessagePlaceholderProcessor.php @@ -65,10 +65,8 @@ public function process( name: 'ORGANIZATION_NAME', resolver: fn(PlaceholderContext $ctx) => $this->config->getValue(ConfigOption::OrganisationName) ?? '' ); - - foreach ($this->placeholderResolvers as $placeholderResolver) { - $resolver->register($placeholderResolver->name(), $placeholderResolver); - } + + $this->registerNestedResolvers($resolver); foreach ($this->patternResolvers as $patternResolver) { $resolver->registerPattern($patternResolver->pattern(), $patternResolver); @@ -136,4 +134,57 @@ private function registerAttributeResolvers( ); } } + + private function maskFooterPlaceholders(string $value, array &$placeholderMap): string + { + $placeholderMap = []; + $index = 0; + + return preg_replace_callback( + '/\[FOOTER(?:%%[^\]]+)?\]/i', + function (array $matches) use (&$placeholderMap, &$index): string { + $token = sprintf('__PHPLIST_FOOTER_TOKEN_%d__', $index++); + $placeholderMap[$token] = $matches[0]; + + return $token; + }, + $value + ) ?? $value; + } + + /** @param array $placeholderMap */ + private function restoreFooterPlaceholders(string $value, array $placeholderMap): string + { + if ($placeholderMap === []) { + return $value; + } + + return strtr($value, $placeholderMap); + } + + private function registerNestedResolvers(PlaceholderResolver $resolver): void + { + foreach ($this->placeholderResolvers as $placeholderResolver) { + if (strtoupper($placeholderResolver->name()) !== 'FOOTER') { + $resolver->register($placeholderResolver->name(), $placeholderResolver); + continue; + } + + $resolver->register( + $placeholderResolver->name(), + function (PlaceholderContext $ctx) use ($placeholderResolver, $resolver): string { + $footer = (string) $placeholderResolver($ctx); + if (!str_contains($footer, '[')) { + return $footer; + } + + $placeholderMap = []; + $maskedFooter = $this->maskFooterPlaceholders($footer, $placeholderMap); + $resolvedFooter = $resolver->resolve($maskedFooter, $ctx); + + return $this->restoreFooterPlaceholders($resolvedFooter, $placeholderMap); + } + ); + } + } } diff --git a/src/Domain/Configuration/Service/Placeholder/FooterValueResolver.php b/src/Domain/Configuration/Service/Placeholder/FooterValueResolver.php index 506581dd..96e66e77 100644 --- a/src/Domain/Configuration/Service/Placeholder/FooterValueResolver.php +++ b/src/Domain/Configuration/Service/Placeholder/FooterValueResolver.php @@ -7,12 +7,13 @@ use PhpList\Core\Domain\Configuration\Model\ConfigOption; use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider; use PhpList\Core\Domain\Configuration\Model\Dto\PlaceholderContext; +use Symfony\Component\DependencyInjection\Attribute\Autowire; final class FooterValueResolver implements PlaceholderValueResolverInterface { public function __construct( private readonly ConfigProvider $config, - private readonly bool $forwardAlternativeContent, + #[Autowire('%messaging.forward_alternative_content%')] private readonly bool $forwardAlternativeContent, ) { } diff --git a/src/Domain/Configuration/Service/Placeholder/ListsValueResolver.php b/src/Domain/Configuration/Service/Placeholder/ListsValueResolver.php index 5a77deb4..7df7adcc 100644 --- a/src/Domain/Configuration/Service/Placeholder/ListsValueResolver.php +++ b/src/Domain/Configuration/Service/Placeholder/ListsValueResolver.php @@ -6,6 +6,7 @@ use PhpList\Core\Domain\Configuration\Model\Dto\PlaceholderContext; use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Contracts\Translation\TranslatorInterface; final class ListsValueResolver implements PlaceholderValueResolverInterface @@ -13,7 +14,7 @@ final class ListsValueResolver implements PlaceholderValueResolverInterface public function __construct( private readonly SubscriberListRepository $subscriberListRepository, private readonly TranslatorInterface $translator, - private readonly bool $showPrivateLists = false, + #[Autowire('%app.preference_page_show_private_lists%')] private readonly bool $showPrivateLists = false, ) { } diff --git a/src/Domain/Configuration/Service/Placeholder/SignatureValueResolver.php b/src/Domain/Configuration/Service/Placeholder/SignatureValueResolver.php index ebd90daa..b00b9182 100644 --- a/src/Domain/Configuration/Service/Placeholder/SignatureValueResolver.php +++ b/src/Domain/Configuration/Service/Placeholder/SignatureValueResolver.php @@ -7,12 +7,13 @@ use PhpList\Core\Domain\Configuration\Model\ConfigOption; use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider; use PhpList\Core\Domain\Configuration\Model\Dto\PlaceholderContext; +use Symfony\Component\DependencyInjection\Attribute\Autowire; final class SignatureValueResolver implements PlaceholderValueResolverInterface { public function __construct( private readonly ConfigProvider $config, - private readonly bool $emailTextCredits = false, + #[Autowire('%messaging.email_text_credits%')] private readonly bool $emailTextCredits = false, ) { } diff --git a/src/Domain/Identity/Service/PermissionChecker.php b/src/Domain/Identity/Service/PermissionChecker.php index 9986ff59..cce4d514 100644 --- a/src/Domain/Identity/Service/PermissionChecker.php +++ b/src/Domain/Identity/Service/PermissionChecker.php @@ -8,6 +8,7 @@ use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; +use PhpList\Core\Domain\Messaging\Model\ListMessage; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberList; @@ -70,9 +71,7 @@ private function resolveRelatedEntity(DomainModel $resource, string $relatedClas } if ($resource instanceof Message && $relatedClass === SubscriberList::class) { - // todo: check which one is correct - // return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray(); - return $resource->getListMessages()->map(fn($lm) => $lm->getSubscriberList())->toArray(); + return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray(); } return []; diff --git a/src/Domain/Messaging/Command/ProcessQueueCommand.php b/src/Domain/Messaging/Command/ProcessQueueCommand.php index 245bc57a..080c24cb 100644 --- a/src/Domain/Messaging/Command/ProcessQueueCommand.php +++ b/src/Domain/Messaging/Command/ProcessQueueCommand.php @@ -8,7 +8,7 @@ use Doctrine\ORM\EntityManagerInterface; use PhpList\Core\Domain\Configuration\Model\ConfigOption; use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider; -use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessage; use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus; use PhpList\Core\Domain\Messaging\Repository\MessageRepository; use PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator; diff --git a/src/Domain/Messaging/Message/CampaignProcessorMessage.php b/src/Domain/Messaging/Message/CampaignProcessor/CampaignProcessorMessage.php similarity index 88% rename from src/Domain/Messaging/Message/CampaignProcessorMessage.php rename to src/Domain/Messaging/Message/CampaignProcessor/CampaignProcessorMessage.php index 06220106..574149d8 100644 --- a/src/Domain/Messaging/Message/CampaignProcessorMessage.php +++ b/src/Domain/Messaging/Message/CampaignProcessor/CampaignProcessorMessage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Messaging\Message; +namespace PhpList\Core\Domain\Messaging\Message\CampaignProcessor; class CampaignProcessorMessage implements CampaignProcessorMessageInterface { diff --git a/src/Domain/Messaging/Message/CampaignProcessorMessageInterface.php b/src/Domain/Messaging/Message/CampaignProcessor/CampaignProcessorMessageInterface.php similarity index 70% rename from src/Domain/Messaging/Message/CampaignProcessorMessageInterface.php rename to src/Domain/Messaging/Message/CampaignProcessor/CampaignProcessorMessageInterface.php index 0d0bc978..dbf71b25 100644 --- a/src/Domain/Messaging/Message/CampaignProcessorMessageInterface.php +++ b/src/Domain/Messaging/Message/CampaignProcessor/CampaignProcessorMessageInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Messaging\Message; +namespace PhpList\Core\Domain\Messaging\Message\CampaignProcessor; interface CampaignProcessorMessageInterface { diff --git a/src/Domain/Messaging/Message/SyncCampaignProcessorMessage.php b/src/Domain/Messaging/Message/CampaignProcessor/SyncCampaignProcessorMessage.php similarity index 88% rename from src/Domain/Messaging/Message/SyncCampaignProcessorMessage.php rename to src/Domain/Messaging/Message/CampaignProcessor/SyncCampaignProcessorMessage.php index a310129f..463dc96b 100644 --- a/src/Domain/Messaging/Message/SyncCampaignProcessorMessage.php +++ b/src/Domain/Messaging/Message/CampaignProcessor/SyncCampaignProcessorMessage.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Messaging\Message; +namespace PhpList\Core\Domain\Messaging\Message\CampaignProcessor; class SyncCampaignProcessorMessage implements CampaignProcessorMessageInterface { diff --git a/src/Domain/Messaging/Message/CampaignProcessor/TestCampaignProcessorMessage.php b/src/Domain/Messaging/Message/CampaignProcessor/TestCampaignProcessorMessage.php new file mode 100644 index 00000000..961e5406 --- /dev/null +++ b/src/Domain/Messaging/Message/CampaignProcessor/TestCampaignProcessorMessage.php @@ -0,0 +1,34 @@ +messageId = $messageId; + $this->listIds = $listIds ?? []; + $this->subscriberEmails = $subscriberEmails ?? []; + } + + public function getMessageId(): int + { + return $this->messageId; + } + + public function getListIds(): array + { + return $this->listIds; + } + + public function getSubscriberEmails(): array + { + return $this->subscriberEmails; + } +} diff --git a/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php b/src/Domain/Messaging/MessageHandler/CampaignProcessor/CampaignProcessorMessageHandler.php similarity index 96% rename from src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php rename to src/Domain/Messaging/MessageHandler/CampaignProcessor/CampaignProcessorMessageHandler.php index f576d1df..e1aa9219 100644 --- a/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/CampaignProcessor/CampaignProcessorMessageHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\Core\Domain\Messaging\MessageHandler; +namespace PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor; use DateTime; use DateTimeImmutable; @@ -13,8 +13,8 @@ use PhpList\Core\Domain\Messaging\Exception\AttachmentCopyException; use PhpList\Core\Domain\Messaging\Exception\MessageCacheMissingException; use PhpList\Core\Domain\Messaging\Exception\MessageSizeLimitExceededException; -use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage; -use PhpList\Core\Domain\Messaging\Message\SyncCampaignProcessorMessage; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessage; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\SyncCampaignProcessorMessage; use PhpList\Core\Domain\Messaging\Model\Dto\MessagePrecacheDto; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus; @@ -109,7 +109,11 @@ public function __invoke(CampaignProcessorMessage|SyncCampaignProcessorMessage $ // $userSelection = $loadedMessageData['userselection']; $cacheKey = sprintf('messaging.message.base.%d.%d', $campaign->getId(), 0); - if (!$this->precacheService->precacheMessage($campaign, $loadedMessageData)) { + if (!$this->precacheService->precacheMessage( + campaign: $campaign, + loadedMessageData: $loadedMessageData, + isTest: false + )) { $this->updateMessageStatus($campaign, MessageStatus::Suspended); return; @@ -199,7 +203,7 @@ private function handleEmailSending( UserMessage $userMessage, MessagePrecacheDto $precachedContent, ): void { - // todo: check at which point link tracking should be applied (maybe after constructing ful text?) + // todo: check at which point link tracking should be applied (maybe after constructing full text?) $processed = $this->messagePreparator->processMessageLinks( campaignId: $campaign->getId(), cachedMessageDto: $precachedContent, diff --git a/src/Domain/Messaging/MessageHandler/CampaignProcessor/TestCampaignProcessorMessageHandler.php b/src/Domain/Messaging/MessageHandler/CampaignProcessor/TestCampaignProcessorMessageHandler.php new file mode 100644 index 00000000..8ae47ea7 --- /dev/null +++ b/src/Domain/Messaging/MessageHandler/CampaignProcessor/TestCampaignProcessorMessageHandler.php @@ -0,0 +1,167 @@ +messageRepository->findById($data->getMessageId()); + if (!$campaign) { + $this->logger->warning( + $this->translator->trans('Campaign not found or not in submitted status'), + ['campaign_id' => $data->getMessageId()] + ); + + return; + } + + $loadedMessageData = ($this->messageDataLoader)($campaign); + + $cacheKey = sprintf('messaging.message.base.%d.%d.%d', $campaign->getId(), 0, 1); + if (!$this->precacheService->precacheMessage( + campaign: $campaign, + loadedMessageData: $loadedMessageData, + isTest: true + )) { + return; + } + + $subscribers = $this->subscriberProvider->getSubscribersForMessageOrLists(data: $data, campaign: $campaign); + + $this->processSubscribersForCampaign(campaign: $campaign, subscribers: $subscribers, cacheKey: $cacheKey); + } + + private function handleEmailSending( + Message $campaign, + Subscriber $subscriber, + MessagePrecacheDto $precachedContent, + ): void { + // todo: check at which point link tracking should be applied (maybe after constructing full text?) + $processed = $this->messagePreparator->processMessageLinks( + campaignId: $campaign->getId(), + cachedMessageDto: $precachedContent, + subscriber: $subscriber + ); + + try { + $result = $this->campaignEmailBuilder->buildCampaignEmail( + messageId: $campaign->getId(), + data: $processed, + toEmail: $subscriber->getEmail(), + skipBlacklistCheck: false, + inBlast: true, + htmlPref: $subscriber->hasHtmlEmail(), + ); + if ($result === null) { + return; + } + $email = $result[0]; + $this->campaignEmailBuilder->applyCampaignHeaders(email: $email, subscriber: $subscriber); + + $this->mailer->send($email); + ($this->mailSizeChecker)($campaign, $email, $subscriber->hasHtmlEmail()); + } catch (MessageSizeLimitExceededException $e) { + // stop after the first message if size is exceeded + $this->logger->error($e->getMessage(), [ + 'campaign_id' => $campaign->getId(), + ]); + throw $e; + } catch (AttachmentCopyException $e) { + // stop after the first message if size is exceeded + $data = new MessagePrecacheDto(); + $data->subject = $this->translator->trans('phpList system error'); + $data->content = $this->translator->trans($e->getMessage()); + + $email = $this->systemEmailBuilder->buildCampaignEmail( + messageId: $campaign->getId(), + data: $data, + toEmail: $this->configProvider->getValue(ConfigOption::ReportAddress) ?? '', + ); + + $envelope = new Envelope( + sender: new Address($this->bounceEmail, 'PHPList'), + recipients: [new Address($email->getTo()[0]->getAddress())], + ); + $this->mailer->send(message: $email, envelope: $envelope); + + throw $e; + } catch (Throwable $e) { + $this->logger->error($e->getMessage(), [ + 'subscriber_id' => $subscriber->getId(), + 'campaign_id' => $campaign->getId(), + ]); + $this->logger->warning($this->translator->trans('Failed to send to: %email%', [ + '%email%' => $subscriber->getEmail(), + ])); + } + } + + private function processSubscribersForCampaign(Message $campaign, array $subscribers, string $cacheKey): void + { + foreach ($subscribers as $subscriber) { + if (!filter_var($subscriber->getEmail(), FILTER_VALIDATE_EMAIL)) { + continue; + } + + $messagePrecacheDto = $this->cache->get($cacheKey); + if ($messagePrecacheDto === null) { + throw new MessageCacheMissingException(); + } + + $this->handleEmailSending($campaign, $subscriber, $messagePrecacheDto); + } + } +} diff --git a/src/Domain/Messaging/Model/Dto/Message/MessageContentDto.php b/src/Domain/Messaging/Model/Dto/Message/MessageContentDto.php index 0ac18672..c5fb5ce8 100644 --- a/src/Domain/Messaging/Model/Dto/Message/MessageContentDto.php +++ b/src/Domain/Messaging/Model/Dto/Message/MessageContentDto.php @@ -9,7 +9,6 @@ class MessageContentDto public function __construct( public readonly string $subject, public readonly string $text, - public readonly string $textMessage, public readonly string $footer, ) { } diff --git a/src/Domain/Messaging/Model/Dto/Message/MessageFormatDto.php b/src/Domain/Messaging/Model/Dto/Message/MessageFormatDto.php index 01bc0474..d6849d26 100644 --- a/src/Domain/Messaging/Model/Dto/Message/MessageFormatDto.php +++ b/src/Domain/Messaging/Model/Dto/Message/MessageFormatDto.php @@ -6,10 +6,7 @@ class MessageFormatDto { - public function __construct( - public readonly bool $htmlFormated, - public readonly string $sendFormat, - public readonly array $formatOptions, - ) { + public function __construct(public readonly string $sendFormat) + { } } diff --git a/src/Domain/Messaging/Model/Filter/MessageFilter.php b/src/Domain/Messaging/Model/Filter/MessageFilter.php index ef1211cf..ccb5b1ac 100644 --- a/src/Domain/Messaging/Model/Filter/MessageFilter.php +++ b/src/Domain/Messaging/Model/Filter/MessageFilter.php @@ -11,6 +11,7 @@ class MessageFilter extends PaginatedFilter implements FilterRequestInterface { private ?Administrator $owner = null; + private ?string $subject = null; public function getOwner(): ?Administrator { @@ -22,4 +23,18 @@ public function setOwner(?Administrator $admin): self $this->owner = $admin; return $this; } + + public function getSubject(): ?string + { + return $this->subject; + } + + public function setSubject(?string $subject): self + { + if ($subject !== null) { + $subject = trim($subject); + } + $this->subject = $subject; + return $this; + } } diff --git a/src/Domain/Messaging/Model/Message.php b/src/Domain/Messaging/Model/Message.php index c70ff01c..4d5f4e8f 100644 --- a/src/Domain/Messaging/Model/Message.php +++ b/src/Domain/Messaging/Model/Message.php @@ -61,6 +61,9 @@ class Message implements DomainModel, Identity, ModificationDate, OwnableInterfa #[ORM\JoinColumn(name: 'template', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] private ?Template $template = null; + /** + * @var Collection + */ #[ORM\OneToMany(targetEntity: ListMessage::class, mappedBy: 'message')] private Collection $listMessages; @@ -190,6 +193,9 @@ public function setOptions(MessageOptions $options): self return $this; } + /** + * @return Collection + */ public function getListMessages(): Collection { return $this->listMessages; diff --git a/src/Domain/Messaging/Model/Message/MessageFormat.php b/src/Domain/Messaging/Model/Message/MessageFormat.php index 1f857c37..419b63df 100644 --- a/src/Domain/Messaging/Model/Message/MessageFormat.php +++ b/src/Domain/Messaging/Model/Message/MessageFormat.php @@ -55,6 +55,11 @@ public function getSendFormat(): ?string return $this->sendFormat; } + public function isInvitation(): bool + { + return $this->sendFormat === 'invite'; + } + public function setSendFormat(?string $sendFormat): self { $this->sendFormat = $sendFormat; diff --git a/src/Domain/Messaging/Model/Message/MessageMetadata.php b/src/Domain/Messaging/Model/Message/MessageMetadata.php index 4d774143..c571bd1f 100644 --- a/src/Domain/Messaging/Model/Message/MessageMetadata.php +++ b/src/Domain/Messaging/Model/Message/MessageMetadata.php @@ -6,8 +6,8 @@ use DateTime; use Doctrine\ORM\Mapping as ORM; -use InvalidArgumentException; use PhpList\Core\Domain\Common\Model\Interfaces\EmbeddableInterface; +use Symfony\Component\Validator\Exception\ValidatorException; #[ORM\Embeddable] class MessageMetadata implements EmbeddableInterface @@ -57,7 +57,11 @@ public function getStatus(): ?MessageStatus public function setStatus(MessageStatus $status): self { if (!$this->getStatus()->canTransitionTo($status)) { - throw new InvalidArgumentException('Invalid status transition'); + throw new ValidatorException( + 'status: Invalid transition ' . $this->status . ' -> ' . $status->value + . PHP_EOL . + 'metadata.status: Invalid transition ' . $this->status . ' -> ' . $status->value + ); } $this->status = $status->value; diff --git a/src/Domain/Messaging/Model/Message/MessageStatus.php b/src/Domain/Messaging/Model/Message/MessageStatus.php index 90b7f987..789f07c2 100644 --- a/src/Domain/Messaging/Model/Message/MessageStatus.php +++ b/src/Domain/Messaging/Model/Message/MessageStatus.php @@ -22,12 +22,13 @@ enum MessageStatus: string public function allowedTransitions(): array { return match ($this) { - self::Draft, self::Suspended => [self::Submitted], - self::Submitted => [self::Prepared, self::InProcess], - self::Prepared => [self::InProcess], + self::Draft => [self::Prepared, self::Submitted], + self::Suspended => [self::Submitted, self::Requeued], + self::Submitted => [self::Prepared, self::InProcess, self::Suspended], + self::Prepared => [self::InProcess, self::Suspended], self::InProcess => [self::Sent, self::Suspended, self::Submitted], self::Requeued => [self::InProcess, self::Suspended], - self::Sent => [], + self::Sent => [self::Requeued], }; } diff --git a/src/Domain/Messaging/Repository/MessageRepository.php b/src/Domain/Messaging/Repository/MessageRepository.php index 10ee8629..d18ce68b 100644 --- a/src/Domain/Messaging/Repository/MessageRepository.php +++ b/src/Domain/Messaging/Repository/MessageRepository.php @@ -60,6 +60,11 @@ public function getFilteredAfterId(FilterRequestInterface $filter): PaginatedRes ->setParameter('ownerId', $filter->getOwner()->getId()); } + if ($filter instanceof MessageFilter && $filter->getSubject() !== null) { + $queryBuilder->andWhere('m.content.subject LIKE :subject') + ->setParameter('subject', '%' . $filter->getSubject() . '%'); + } + $countQb = clone $queryBuilder; $total = (int) $countQb ->select('COUNT(DISTINCT m.id)') diff --git a/src/Domain/Messaging/Service/Builder/MessageBuilder.php b/src/Domain/Messaging/Service/Builder/MessageBuilder.php index bb7fd852..89935ee0 100644 --- a/src/Domain/Messaging/Service/Builder/MessageBuilder.php +++ b/src/Domain/Messaging/Service/Builder/MessageBuilder.php @@ -27,7 +27,7 @@ public function build(MessageDtoInterface $createMessageDto, object $context = n throw new InvalidContextTypeException(get_debug_type($context)); } - $format = $this->messageFormatBuilder->build($createMessageDto->getFormat()); + $format = $this->messageFormatBuilder->build($createMessageDto); $schedule = $this->messageScheduleBuilder->build($createMessageDto->getSchedule()); $content = $this->messageContentBuilder->build($createMessageDto->getContent()); $options = $this->messageOptionsBuilder->build($createMessageDto->getOptions()); @@ -42,6 +42,7 @@ public function build(MessageDtoInterface $createMessageDto, object $context = n $context->getExisting()->setContent($content); $context->getExisting()->setOptions($options); $context->getExisting()->setTemplate($template); + return $context->getExisting(); } diff --git a/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php b/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php index 806afe00..b1393b2e 100644 --- a/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php +++ b/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php @@ -4,22 +4,31 @@ namespace PhpList\Core\Domain\Messaging\Service\Builder; +use PhpList\Core\Domain\Common\Html2Text; use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException; use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto; use PhpList\Core\Domain\Messaging\Model\Message\MessageContent; class MessageContentBuilder { + public function __construct(private readonly Html2Text $html2Text) + { + } + public function build(object $dto): MessageContent { if (!$dto instanceof MessageContentDto) { throw new InvalidDtoTypeException(get_debug_type($dto)); } + $textMessage = strip_tags($dto->text) !== $dto->text + ? ($this->html2Text)($dto->text) + : $dto->text; + return new MessageContent( subject: $dto->subject, text: $dto->text, - textMessage: $dto->textMessage, + textMessage: $textMessage, footer: $dto->footer ); } diff --git a/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php b/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php index 2bd5cf7f..9e55c830 100644 --- a/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php +++ b/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php @@ -4,21 +4,18 @@ namespace PhpList\Core\Domain\Messaging\Service\Builder; -use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException; -use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto; +use PhpList\Core\Domain\Messaging\Model\Dto\MessageDtoInterface; use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat; class MessageFormatBuilder { - public function build(object $dto): MessageFormat + public function build(MessageDtoInterface $dto): MessageFormat { - if (!$dto instanceof MessageFormatDto) { - throw new InvalidDtoTypeException(get_debug_type($dto)); - } + $htmlFormatted = strip_tags($dto->getContent()->text) !== $dto->getContent()->text; return new MessageFormat( - htmlFormatted: $dto->htmlFormated, - sendFormat: $dto->sendFormat, + htmlFormatted: $htmlFormatted, + sendFormat: $dto->getFormat()->sendFormat, ); } } diff --git a/src/Domain/Messaging/Service/Manager/MessageManager.php b/src/Domain/Messaging/Service/Manager/MessageManager.php index c11d34d9..7ed34594 100644 --- a/src/Domain/Messaging/Service/Manager/MessageManager.php +++ b/src/Domain/Messaging/Service/Manager/MessageManager.php @@ -8,18 +8,17 @@ use PhpList\Core\Domain\Messaging\Model\Dto\MessageContext; use PhpList\Core\Domain\Messaging\Model\Dto\MessageDtoInterface; use PhpList\Core\Domain\Messaging\Model\Message; +use PhpList\Core\Domain\Messaging\Model\Message\MessageMetadata; use PhpList\Core\Domain\Messaging\Repository\MessageRepository; use PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder; +use Symfony\Component\Validator\Exception\ValidatorException; class MessageManager { - private MessageRepository $messageRepository; - private MessageBuilder $messageBuilder; - - public function __construct(MessageRepository $messageRepository, MessageBuilder $messageBuilder) - { - $this->messageRepository = $messageRepository; - $this->messageBuilder = $messageBuilder; + public function __construct( + private readonly MessageRepository $messageRepository, + private readonly MessageBuilder $messageBuilder, + ) { } public function createMessage(MessageDtoInterface $createMessageDto, Administrator $authUser): Message @@ -31,6 +30,27 @@ public function createMessage(MessageDtoInterface $createMessageDto, Administrat return $message; } + public function copyAsDraftMessage(Message $message, Administrator $authUser): Message + { + $newMessage = new Message( + format: new Message\MessageFormat( + htmlFormatted: $message->getFormat()->isHtmlFormatted(), + sendFormat: $message->getFormat()->getSendFormat() + ), + schedule: clone $message->getSchedule(), + metadata: new MessageMetadata(status: Message\MessageStatus::Draft), + content: clone $message->getContent(), + options: clone $message->getOptions(), + owner: $authUser, + template: $message->getTemplate(), + ); + $newMessage->setUuid(bin2hex(random_bytes(18))); + + $this->messageRepository->persist($newMessage); + + return $newMessage; + } + public function updateMessage( MessageDtoInterface $updateMessageDto, Message $message, @@ -42,6 +62,29 @@ public function updateMessage( public function updateStatus(Message $message, Message\MessageStatus $status): Message { + if ($status === Message\MessageStatus::Submitted && !$this->canBeSubmitted($message)) { + throw new ValidatorException( + 'status: Cannot submit. Add at least one list and fill subject, from field, and message body' + . PHP_EOL . + 'metadata.status: Cannot submit. Add at least one list and fill subject, from field, and message body' + ); + } + + if ($status === Message\MessageStatus::Submitted + && $message->getFormat()->isInvitation() + && !str_contains($message->getContent()->getText(), '[CONFIRMATIONURL]') + ) { + throw new ValidatorException( + 'status: Cannot submit invitation. Add [CONFIRMATIONURL] placeholder to the message body' + . PHP_EOL . + 'metadata.status: Cannot submit invitation. Add [CONFIRMATIONURL] placeholder to the message body' + ); + } + + if ($status === Message\MessageStatus::Submitted && $message->getListMessages()->count() === 0) { + $status = Message\MessageStatus::Prepared; + } + $message->getMetadata()->setStatus($status); return $message; @@ -57,4 +100,17 @@ public function getMessagesByOwner(Administrator $owner): array { return $this->messageRepository->getByOwnerId($owner->getId()); } + + private function canBeSubmitted(Message $message): bool + { + return $message->getListMessages()->count() > 0 + && $this->isFilled($message->getContent()->getSubject()) + && $this->isFilled($message->getOptions()->getFromField()) + && $this->isFilled($message->getContent()->getText()); + } + + private function isFilled(?string $value): bool + { + return !empty(trim((string) $value)); + } } diff --git a/src/Domain/Messaging/Service/MessageDataLoader.php b/src/Domain/Messaging/Service/MessageDataLoader.php index c3577bec..9e5d07c8 100644 --- a/src/Domain/Messaging/Service/MessageDataLoader.php +++ b/src/Domain/Messaging/Service/MessageDataLoader.php @@ -109,6 +109,10 @@ private function mergeNonEmptyFields(array &$messageData, Message $message): voi if ($messageData['subject'] === '(no title)') { $messageData['subject'] = '(no subject)'; } + + if ($messageData['subject'] === '') { + $messageData['subject'] = '(no subject)'; + } } private function mergeStoredMessageData(array &$messageData, Message $message): void @@ -148,7 +152,7 @@ private function normaliseScheduleFields(array &$messageData): void private function populateTargetLists(array &$messageData, Message $message): void { foreach ($message->getListMessages() as $listMessage) { - $messageData['targetlist'][$listMessage->getListId()] = 1; + $messageData['targetlist'][$listMessage->getList()->getId()] = 1; } } diff --git a/src/Domain/Messaging/Service/MessagePrecacheService.php b/src/Domain/Messaging/Service/MessagePrecacheService.php index dfab25ec..9ea7e873 100644 --- a/src/Domain/Messaging/Service/MessagePrecacheService.php +++ b/src/Domain/Messaging/Service/MessagePrecacheService.php @@ -37,6 +37,7 @@ public function __construct( private readonly bool $useManualTextPart, private readonly string $uploadImageDir, private readonly string $publicSchema, + private readonly ?int $ttl = 3600, ) { } @@ -44,11 +45,20 @@ public function __construct( * Retrieve the base (unpersonalized) message content for a campaign from cache, * or cache it on first access. Handle [URL:] token fetch and basic placeholder replacements. */ - public function precacheMessage(Message $campaign, array $loadedMessageData, ?bool $forwardContent = false): bool - { - $cacheKey = sprintf('messaging.message.base.%d.%d', $campaign->getId(), (int) $forwardContent); + public function precacheMessage( + Message $campaign, + array $loadedMessageData, + ?bool $forwardContent = false, + ?bool $isTest = false, + ): bool { + $cacheKey = sprintf( + 'messaging.message.base.%d.%d.%d', + $campaign->getId(), + (int) $forwardContent, + (int) $isTest + ); $cached = $this->cache->get($cacheKey); - if ($cached !== null) { + if ($cached !== null && $isTest === false) { return true; } @@ -71,7 +81,7 @@ public function precacheMessage(Message $campaign, array $loadedMessageData, ?bo return false; } - $messagePrecacheDto->googleTrack = $loadedMessageData['google_track']; + $messagePrecacheDto->googleTrack = (bool) $loadedMessageData['google_track']; $this->applyBasicReplacements($messagePrecacheDto, $loadedMessageData); $this->populateAdminAttributes($messagePrecacheDto, $campaign); @@ -93,7 +103,8 @@ public function precacheMessage(Message $campaign, array $loadedMessageData, ?bo $messagePrecacheDto->htmlFooter = $this->templateImageManager ->parseLogoPlaceholders($messagePrecacheDto->htmlFooter); - $this->cache->set($cacheKey, $messagePrecacheDto); + $ttl = $isTest ? 5 : $this->ttl; + $this->cache->set(key: $cacheKey, value: $messagePrecacheDto, ttl: $ttl); return true; } diff --git a/src/Domain/Messaging/Service/MessageProcessingPreparator.php b/src/Domain/Messaging/Service/MessageProcessingPreparator.php index 960e17e4..05f044ea 100644 --- a/src/Domain/Messaging/Service/MessageProcessingPreparator.php +++ b/src/Domain/Messaging/Service/MessageProcessingPreparator.php @@ -4,6 +4,7 @@ namespace PhpList\Core\Domain\Messaging\Service; +use PhpList\Core\Domain\Analytics\Model\LinkTrack; use PhpList\Core\Domain\Analytics\Service\LinkTrackService; use PhpList\Core\Domain\Messaging\Model\Dto\MessagePrecacheDto; use PhpList\Core\Domain\Messaging\Repository\MessageRepository; @@ -68,9 +69,9 @@ public function processMessageLinks( } $savedLinks = $this->linkTrackService->extractAndSaveLinks( - $cachedMessageDto, - $subscriber->getId(), - $campaignId, + content: $cachedMessageDto, + userId: $subscriber->getId(), + messageId: $campaignId, ); if (empty($savedLinks)) { return $cachedMessageDto; @@ -78,11 +79,17 @@ public function processMessageLinks( // todo: check if getTextMessage should replace links as well if ($cachedMessageDto->content) { - $cachedMessageDto->content = $this->replaceLinks($savedLinks, $cachedMessageDto->content); + $cachedMessageDto->content = $this->replaceLinks( + savedLinks: $savedLinks, + htmlText: $cachedMessageDto->content + ); } if ($cachedMessageDto->htmlFooter) { - $cachedMessageDto->htmlFooter = $this->replaceLinks($savedLinks, $cachedMessageDto->htmlFooter); + $cachedMessageDto->htmlFooter = $this->replaceLinks( + savedLinks: $savedLinks, + htmlText: $cachedMessageDto->htmlFooter + ); } return $cachedMessageDto; @@ -90,6 +97,7 @@ public function processMessageLinks( private function replaceLinks(array $savedLinks, string $htmlText): string { + /** @var LinkTrack $linkTrack */ foreach ($savedLinks as $linkTrack) { $originalUrl = $linkTrack->getUrl(); $trackUrl = self::LINK_TRACK_ENDPOINT . '?id=' . $linkTrack->getId(); diff --git a/src/Domain/Subscription/Repository/SubscriberRepository.php b/src/Domain/Subscription/Repository/SubscriberRepository.php index 2e977cff..4fbfac0a 100644 --- a/src/Domain/Subscription/Repository/SubscriberRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberRepository.php @@ -328,4 +328,19 @@ public function countCreatedBetween(DateTimeInterface $start, DateTimeInterface ->getQuery() ->getSingleScalarResult(); } + + /** @return Subscriber[] */ + public function getByEmails(array $emails): array + { + if (empty($emails)) { + return []; + } + + return $this->createQueryBuilder('s') + ->select('s') + ->where('s.email IN (:emails)') + ->setParameter('emails', $emails) + ->getQuery() + ->getResult(); + } } diff --git a/src/Domain/Subscription/Service/Provider/SubscriberProvider.php b/src/Domain/Subscription/Service/Provider/SubscriberProvider.php index da564da8..758db32e 100644 --- a/src/Domain/Subscription/Service/Provider/SubscriberProvider.php +++ b/src/Domain/Subscription/Service/Provider/SubscriberProvider.php @@ -4,7 +4,8 @@ namespace PhpList\Core\Domain\Subscription\Service\Provider; -use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessageInterface; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessageInterface; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\TestCampaignProcessorMessage; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; @@ -32,6 +33,10 @@ public function __construct( */ public function getSubscribersForMessageOrLists(CampaignProcessorMessageInterface $data, Message $campaign): array { + if ($data instanceof TestCampaignProcessorMessage) { + return $this->subscriberRepository->getByEmails($data->getSubscriberEmails()); + } + if (count($data->getListIds()) > 0) { $listIds = $data->getListIds(); } else { diff --git a/tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php b/tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php index a2a53892..c7495f99 100644 --- a/tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php +++ b/tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php @@ -4,6 +4,7 @@ namespace PhpList\Core\Tests\Unit\Domain\Analytics\Service; +use Doctrine\ORM\EntityManagerInterface; use PhpList\Core\Core\ParameterProvider; use PhpList\Core\Domain\Analytics\Exception\MissingMessageIdException; use PhpList\Core\Domain\Analytics\Model\LinkTrack; @@ -29,7 +30,11 @@ protected function setUp(): void ->with('click_track', false) ->willReturn(true); - $this->subject = new LinkTrackService($this->linkTrackRepository, $paramProvider); + $this->subject = new LinkTrackService( + linkTrackRepository: $this->linkTrackRepository, + paramProvider: $paramProvider, + entityManager: $this->createMock(EntityManagerInterface::class) + ); } public function testExtractAndSaveLinksWithNoLinks(): void @@ -209,7 +214,11 @@ public function testIsExtractAndSaveLinksApplicableWhenClickTrackIsFalse(): void ->with('click_track', false) ->willReturn(false); - $subject = new LinkTrackService($this->linkTrackRepository, $paramProvider); + $subject = new LinkTrackService( + linkTrackRepository: $this->linkTrackRepository, + paramProvider: $paramProvider, + entityManager: $this->createMock(EntityManagerInterface::class) + ); self::assertFalse($subject->isExtractAndSaveLinksApplicable()); } diff --git a/tests/Unit/Domain/Configuration/Service/MessagePlaceholderProcessorTest.php b/tests/Unit/Domain/Configuration/Service/MessagePlaceholderProcessorTest.php index 2d49fa39..77cfe0f9 100644 --- a/tests/Unit/Domain/Configuration/Service/MessagePlaceholderProcessorTest.php +++ b/tests/Unit/Domain/Configuration/Service/MessagePlaceholderProcessorTest.php @@ -190,4 +190,42 @@ public function resolve(string $key, PlaceholderContext $ctx): ?string $this->assertStringContainsString('A XVAL B ABC C SVAL', $out); } + + public function testFooterContentIsResolvedForNestedPlaceholders(): void + { + $user = $this->makeUser('nest@example.com'); + $dto = new MessagePrecacheDto(); + + $footerResolver = new class implements PlaceholderValueResolverInterface { + public function name(): string + { + return 'FOOTER'; + } + + public function __invoke(PlaceholderContext $ctx): string + { + return 'Footer contact [EMAIL]'; + } + }; + + $processor = new MessagePlaceholderProcessor( + config: $this->config, + attributesRepository: $this->attrRepo, + attributeValueResolver: $this->attrResolver, + placeholderResolvers: [$footerResolver], + patternResolvers: [], + supportingResolvers: [], + alwaysAddUserTrack: false, + keepForwardedAttributes: false + ); + + $out = $processor->process( + value: 'Body [FOOTER]', + receiver: $user, + format: OutputFormat::Text, + messagePrecacheDto: $dto, + ); + + $this->assertStringContainsString('Body Footer contact nest@example.com', $out); + } } diff --git a/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php b/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php index e4dc836e..3f5adf27 100644 --- a/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php +++ b/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php @@ -8,7 +8,7 @@ use Exception; use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider; use PhpList\Core\Domain\Messaging\Command\ProcessQueueCommand; -use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessage; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Messaging\Repository\MessageRepository; use PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator; diff --git a/tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php b/tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php index 96b7dadd..0d05bd7a 100644 --- a/tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php +++ b/tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php @@ -8,9 +8,8 @@ use Exception; use PhpList\Core\Domain\Configuration\Model\OutputFormat; use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider; -use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage; -use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessageInterface; -use PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessage; +use PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\CampaignProcessorMessageHandler; use PhpList\Core\Domain\Messaging\Model\Dto\MessagePrecacheDto; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Messaging\Model\Message\MessageContent; @@ -24,8 +23,8 @@ use PhpList\Core\Domain\Messaging\Service\MailSizeChecker; use PhpList\Core\Domain\Messaging\Service\MaxProcessTimeLimiter; use PhpList\Core\Domain\Messaging\Service\MessageDataLoader; -use PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator; use PhpList\Core\Domain\Messaging\Service\MessagePrecacheService; +use PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator; use PhpList\Core\Domain\Messaging\Service\RateLimitedCampaignMailer; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; diff --git a/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php index d08ee9a1..6fba68db 100644 --- a/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php @@ -27,6 +27,7 @@ class MessageBuilderTest extends TestCase { + private TemplateRepository&MockObject $templateRepository; private MessageFormatBuilder&MockObject $formatBuilder; private MessageScheduleBuilder&MockObject $scheduleBuilder; private MessageContentBuilder&MockObject $contentBuilder; @@ -35,14 +36,14 @@ class MessageBuilderTest extends TestCase protected function setUp(): void { - $templateRepository = $this->createMock(TemplateRepository::class); + $this->templateRepository = $this->createMock(TemplateRepository::class); $this->formatBuilder = $this->createMock(MessageFormatBuilder::class); $this->scheduleBuilder = $this->createMock(MessageScheduleBuilder::class); $this->contentBuilder = $this->createMock(MessageContentBuilder::class); $this->optionsBuilder = $this->createMock(MessageOptionsBuilder::class); $this->builder = new MessageBuilder( - templateRepository: $templateRepository, + templateRepository: $this->templateRepository, messageFormatBuilder: $this->formatBuilder, messageScheduleBuilder: $this->scheduleBuilder, messageContentBuilder: $this->contentBuilder, @@ -50,19 +51,16 @@ protected function setUp(): void ); } - private function createRequest(): CreateMessageDto + private function createRequest(?int $templateId = 0): CreateMessageDto { return new CreateMessageDto( content: new MessageContentDto( subject: '', text: '', - textMessage: '', footer: '' ), format: new MessageFormatDto( - htmlFormated: false, sendFormat: 'text', - formatOptions: [] ), metadata: new MessageMetadataDto( status: Message\MessageStatus::Draft @@ -80,7 +78,7 @@ private function createRequest(): CreateMessageDto requeueInterval: null, requeueUntil: null ), - templateId: 0 + templateId: $templateId ); } @@ -88,7 +86,7 @@ private function mockBuildCalls(CreateMessageDto $createMessageDto): void { $this->formatBuilder->expects($this->once()) ->method('build') - ->with($createMessageDto->format) + ->with($createMessageDto) ->willReturn($this->createMock(Message\MessageFormat::class)); $this->scheduleBuilder->expects($this->once()) @@ -115,7 +113,14 @@ public function testBuildsNewMessage(): void $this->mockBuildCalls($request); - $this->builder->build(createMessageDto: $request, context: $context); + $this->templateRepository->expects($this->once()) + ->method('find') + ->with(0) + ->willReturn(null); + + $result = $this->builder->build(createMessageDto: $request, context: $context); + + $this->assertInstanceOf(Message::class, $result); } public function testThrowsExceptionOnInvalidContext(): void @@ -134,25 +139,37 @@ public function testUpdatesExistingMessage(): void $this->mockBuildCalls($request); + $this->templateRepository->expects($this->once()) + ->method('find') + ->with(0) + ->willReturn(null); + $existingMessage ->expects($this->once()) ->method('setFormat') ->with($this->isInstanceOf(Message\MessageFormat::class)); + $existingMessage ->expects($this->once()) ->method('setSchedule') ->with($this->isInstanceOf(MessageSchedule::class)); + $existingMessage ->expects($this->once()) ->method('setContent') ->with($this->isInstanceOf(MessageContent::class)); + $existingMessage ->expects($this->once()) ->method('setOptions') ->with($this->isInstanceOf(Message\MessageOptions::class)); - $existingMessage->expects($this->once())->method('setTemplate')->with(null); - $result = $this->builder->build($request, $context); + $existingMessage + ->expects($this->once()) + ->method('setTemplate') + ->with(null); + + $result = $this->builder->build(createMessageDto: $request, context: $context); $this->assertSame($existingMessage, $result); } diff --git a/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php index 62475884..82fab153 100644 --- a/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php @@ -4,26 +4,33 @@ namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Builder; +use PhpList\Core\Domain\Common\Html2Text; use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException; use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto; use PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class MessageContentBuilderTest extends TestCase { private MessageContentBuilder $builder; + private Html2Text&MockObject $html2Text; protected function setUp(): void { - $this->builder = new MessageContentBuilder(); + $this->html2Text = $this->createMock(Html2Text::class); + $this->builder = new MessageContentBuilder($this->html2Text); } public function testBuildsMessageContentSuccessfully(): void { + $this->html2Text + ->expects($this->never()) + ->method('__invoke'); + $dto = new MessageContentDto( subject: 'Test Subject', text: 'Full text content', - textMessage: 'Short text version', footer: 'Footer text' ); @@ -31,14 +38,37 @@ public function testBuildsMessageContentSuccessfully(): void $this->assertSame('Test Subject', $messageContent->getSubject()); $this->assertSame('Full text content', $messageContent->getText()); - $this->assertSame('Short text version', $messageContent->getTextMessage()); + $this->assertSame('Full text content', $messageContent->getTextMessage()); $this->assertSame('Footer text', $messageContent->getFooter()); } + public function testBuildsPlainTextMessageFromHtmlText(): void + { + $dto = new MessageContentDto( + subject: 'Test Subject', + text: '

Full text content

', + footer: 'Footer text' + ); + + $this->html2Text + ->expects($this->once()) + ->method('__invoke') + ->with('

Full text content

') + ->willReturn('Full text content'); + + $messageContent = $this->builder->build($dto); + + $this->assertSame('Full text content', $messageContent->getTextMessage()); + } + public function testThrowsExceptionOnInvalidDto(): void { $this->expectException(InvalidDtoTypeException::class); + $this->html2Text + ->expects($this->never()) + ->method('__invoke'); + $invalidDto = new \stdClass(); $this->builder->build($invalidDto); } diff --git a/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php index ed4645ed..0e682524 100644 --- a/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php +++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php @@ -4,34 +4,51 @@ namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Builder; -use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException; +use PhpList\Core\Domain\Messaging\Model\Dto\CreateMessageDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto; use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto; -use PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageMetadataDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageOptionsDto; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageScheduleDto; use PHPUnit\Framework\TestCase; +use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat; +use PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder; -class MessageFormatBuilderTest extends TestCase +final class MessageFormatBuilderTest extends TestCase { - private MessageFormatBuilder $builder; - - protected function setUp(): void + public function testBuildSetsHtmlFormattedToFalseWhenContentIsPlainText(): void { - $this->builder = new MessageFormatBuilder(); + $dto = $this->createDto('Plain text content', 'text'); + + $builder = new MessageFormatBuilder(); + $result = $builder->build($dto); + + self::assertInstanceOf(MessageFormat::class, $result); + self::assertFalse($result->isHtmlFormatted()); + self::assertSame('text', $result->getSendFormat()); } - public function testBuildsMessageFormatSuccessfully(): void + public function testBuildSetsHtmlFormattedToTrueWhenContentContainsHtml(): void { - $dto = new MessageFormatDto(htmlFormated: true, sendFormat: 'html', formatOptions: ['html', 'text']); - $messageFormat = $this->builder->build($dto); + $dto = $this->createDto('

Hello world

', 'html'); + + $builder = new MessageFormatBuilder(); + $result = $builder->build($dto); - $this->assertSame(true, $messageFormat->isHtmlFormatted()); - $this->assertSame('html', $messageFormat->getSendFormat()); + self::assertInstanceOf(MessageFormat::class, $result); + self::assertTrue($result->isHtmlFormatted()); + self::assertSame('html', $result->getSendFormat()); } - public function testThrowsExceptionOnInvalidDto(): void + private function createDto(string $text, string $sendFormat): CreateMessageDto { - $this->expectException(InvalidDtoTypeException::class); - - $invalidDto = new \stdClass(); - $this->builder->build($invalidDto); + return new CreateMessageDto( + content: new MessageContentDto(subject: '', text: $text, footer: ''), + format: new MessageFormatDto(sendFormat: $sendFormat), + metadata: $this->createMock(MessageMetadataDto::class), + options: $this->createMock(MessageOptionsDto::class), + schedule: $this->createMock(MessageScheduleDto::class), + templateId: null, + ); } } diff --git a/tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php b/tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php index cbbbcb68..df00e584 100644 --- a/tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php +++ b/tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php @@ -4,7 +4,10 @@ namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Manager; +use DateTime; +use InvalidArgumentException; use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Messaging\Model\ListMessage; use PhpList\Core\Domain\Messaging\Model\Dto\CreateMessageDto; use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto; use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto; @@ -13,21 +16,65 @@ use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageScheduleDto; use PhpList\Core\Domain\Messaging\Model\Dto\UpdateMessageDto; use PhpList\Core\Domain\Messaging\Model\Message; +use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat; +use PhpList\Core\Domain\Messaging\Model\Message\MessageMetadata; +use PhpList\Core\Domain\Messaging\Model\Message\MessageOptions; +use PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule; use PhpList\Core\Domain\Messaging\Model\Message\MessageContent; use PhpList\Core\Domain\Messaging\Repository\MessageRepository; use PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder; use PhpList\Core\Domain\Messaging\Service\Manager\MessageManager; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Exception\ValidatorException; class MessageManagerTest extends TestCase { + public function testCopyAsDraftMessagePersistsClonedDraftMessage(): void + { + $messageRepository = $this->createMock(MessageRepository::class); + $messageBuilder = $this->createMock(MessageBuilder::class); + $manager = new MessageManager($messageRepository, $messageBuilder); + + $message = new Message( + format: new MessageFormat(true, 'html'), + schedule: new MessageSchedule( + repeatInterval: 0, + repeatUntil: null, + requeueInterval: 0, + requeueUntil: null, + embargo: new DateTime('2025-04-17T09:00:00+00:00') + ), + metadata: new MessageMetadata(Message\MessageStatus::Submitted), + content: new MessageContent('Subject', 'Full text', 'Short text', 'Footer'), + options: new MessageOptions('from@example.com', 'to@example.com', 'reply@example.com', 'all-users'), + owner: null + ); + + $messageRepository->expects($this->once()) + ->method('persist') + ->with($this->callback(function (Message $persistedMessage) use ($message): bool { + $this->assertNotSame($message, $persistedMessage); + $this->assertSame(Message\MessageStatus::Draft, $persistedMessage->getMetadata()->getStatus()); + $this->assertTrue($persistedMessage->getFormat()->isHtmlFormatted()); + $this->assertSame('html', $persistedMessage->getFormat()->getSendFormat()); + + return true; + })); + + $result = $manager->copyAsDraftMessage($message, $this->createMock(Administrator::class)); + + $this->assertSame(Message\MessageStatus::Draft, $result->getMetadata()->getStatus()); + $this->assertNotSame($message, $result); + } + public function testCreateMessageReturnsPersistedMessage(): void { $messageRepository = $this->createMock(MessageRepository::class); $messageBuilder = $this->createMock(MessageBuilder::class); $manager = new MessageManager($messageRepository, $messageBuilder); - $format = new MessageFormatDto(true, 'html', ['html']); + $format = new MessageFormatDto('html'); $schedule = new MessageScheduleDto( embargo: '2025-04-17T09:00:00+00:00', repeatInterval: 60 * 24, @@ -36,7 +83,7 @@ public function testCreateMessageReturnsPersistedMessage(): void requeueUntil: '2025-04-20T00:00:00+00:00', ); $metadata = new MessageMetadataDto(Message\MessageStatus::Draft); - $content = new MessageContentDto('Subject', 'Full text', 'Short text', 'Footer'); + $content = new MessageContentDto(subject: 'Subject', text: 'Full text', footer: 'Footer'); $options = new MessageOptionsDto('from@example.com', 'to@example.com', 'reply@example.com', 'all-users'); $request = new CreateMessageDto( @@ -81,7 +128,7 @@ public function testUpdateMessageReturnsUpdatedMessage(): void $messageBuilder = $this->createMock(MessageBuilder::class); $manager = new MessageManager($messageRepository, $messageBuilder); - $format = new MessageFormatDto(false, 'text', ['text']); + $format = new MessageFormatDto('text'); $schedule = new MessageScheduleDto( embargo: '2025-04-17T09:00:00+00:00', repeatInterval: 0, @@ -91,16 +138,15 @@ public function testUpdateMessageReturnsUpdatedMessage(): void ); $metadata = new MessageMetadataDto(Message\MessageStatus::Draft); $content = new MessageContentDto( - 'Updated Subject', - 'Updated Full text', - 'Updated Short text', - 'Updated Footer' + subject: 'Updated Subject', + text: 'Updated Full text', + footer: 'Updated Footer' ); $options = new MessageOptionsDto( - 'newfrom@example.com', - 'newto@example.com', - 'newreply@example.com', - 'active-users' + fromField: 'newfrom@example.com', + toField: 'newto@example.com', + replyTo: 'newreply@example.com', + userSelection: 'active-users' ); $updateRequest = new UpdateMessageDto( @@ -134,4 +180,63 @@ public function testUpdateMessageReturnsUpdatedMessage(): void $this->assertSame('Updated Subject', $message->getContent()->getSubject()); $this->assertSame(Message\MessageStatus::Draft, $message->getMetadata()->getStatus()); } + + public function testUpdateStatusThrowsWhenSubmittedWithoutListMessage(): void + { + $messageRepository = $this->createMock(MessageRepository::class); + $messageBuilder = $this->createMock(MessageBuilder::class); + $manager = new MessageManager($messageRepository, $messageBuilder); + + $message = new Message( + format: new MessageFormat(true, 'html'), + schedule: new MessageSchedule( + repeatInterval: 0, + repeatUntil: null, + requeueInterval: 0, + requeueUntil: null, + embargo: new DateTime('2025-04-17T09:00:00+00:00') + ), + metadata: new MessageMetadata(Message\MessageStatus::Draft), + content: new MessageContent('Subject', 'Body text', 'Short text', 'Footer'), + options: new MessageOptions('from@example.com', 'to@example.com', 'reply@example.com', 'all-users'), + owner: null + ); + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessage('Cannot submit.'); + + $manager->updateStatus($message, Message\MessageStatus::Submitted); + } + + public function testUpdateStatusSetsSubmittedWhenRequiredFieldsAndListArePresent(): void + { + $messageRepository = $this->createMock(MessageRepository::class); + $messageBuilder = $this->createMock(MessageBuilder::class); + $manager = new MessageManager($messageRepository, $messageBuilder); + + $message = new Message( + format: new MessageFormat(true, 'html'), + schedule: new MessageSchedule( + repeatInterval: 0, + repeatUntil: null, + requeueInterval: 0, + requeueUntil: null, + embargo: new DateTime('2025-04-17T09:00:00+00:00') + ), + metadata: new MessageMetadata(Message\MessageStatus::Draft), + content: new MessageContent('Subject', 'Body text', 'Short text', 'Footer'), + options: new MessageOptions('from@example.com', 'to@example.com', 'reply@example.com', 'all-users'), + owner: null + ); + + $listMessage = new ListMessage(); + $listMessage->setMessage($message); + $listMessage->setList($this->createMock(SubscriberList::class)); + $message->getListMessages()->add($listMessage); + + $updated = $manager->updateStatus($message, Message\MessageStatus::Submitted); + + $this->assertSame($message, $updated); + $this->assertSame(Message\MessageStatus::Submitted, $message->getMetadata()->getStatus()); + } } diff --git a/tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php b/tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php index c6174a44..5999ce50 100644 --- a/tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php +++ b/tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php @@ -7,11 +7,13 @@ use Doctrine\Common\Collections\ArrayCollection; use PhpList\Core\Domain\Configuration\Model\ConfigOption; use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider; +use PhpList\Core\Domain\Messaging\Model\ListMessage; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Messaging\Model\MessageData; use PhpList\Core\Domain\Messaging\Repository\MessageDataRepository; use PhpList\Core\Domain\Messaging\Repository\MessageRepository; use PhpList\Core\Domain\Messaging\Service\MessageDataLoader; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -61,6 +63,11 @@ public function testLoadsMessageDataMergesAndParses(): void $md2 = (new MessageData())->setId($messageId)->setName('criteria_match')->setData('any'); $md3 = (new MessageData())->setId($messageId)->setName('embargo')->setData('string'); + $listMock = $this->createMock(SubscriberList::class); + $listMock->method('getId')->willReturn(42); + $listMessageMock = $this->createMock(ListMessage::class); + $listMessageMock->method('getList')->willReturn($listMock); + $this->messageDataRepository ->method('getForMessage') ->with($messageId) @@ -69,16 +76,7 @@ public function testLoadsMessageDataMergesAndParses(): void // Use a Message mock instead of an anonymous stub $message = $this->createMock(Message::class); $message->method('getId')->willReturn($messageId); - $message->method('getListMessages')->willReturn( - new ArrayCollection([ - new class { - public function getListId(): int - { - return 42; - } - }, - ]) - ); + $message->method('getListMessages')->willReturn(new ArrayCollection([$listMessageMock])); $loader = new MessageDataLoader( configProvider: $this->config, diff --git a/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php b/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php index fa304c8a..ee263b73 100644 --- a/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php +++ b/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php @@ -4,8 +4,8 @@ namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service\Provider; -use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage; -use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessageInterface; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessage; +use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessageInterface; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository;