Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a2d50a1
Style: Update constructor
tatevikg1 Dec 30, 2025
216978e
UserPersonalizer
tatevikg1 Dec 30, 2025
968044a
Use correct mailer
tatevikg1 Jan 2, 2026
78146e5
refactor
tatevikg1 Jan 4, 2026
975b418
RateLimitedCampaignMailer
tatevikg1 Jan 4, 2026
9b45ab3
PlaceholderValueResolverInterface
tatevikg1 Jan 7, 2026
8ed86ed
FORWARDEDBY
tatevikg1 Jan 8, 2026
5bed8e2
ForwardValueResolver
tatevikg1 Jan 8, 2026
5fdae5b
PreferencesValueResolver
tatevikg1 Jan 8, 2026
9d07526
SignatureValueResolver
tatevikg1 Jan 8, 2026
de8f4a8
MailConstructor
tatevikg1 Jan 8, 2026
54368f1
Append footer and signature if not prsent
tatevikg1 Jan 12, 2026
3728a1a
Rename UserPersonalizer to MessagePlaceholderProcessor
tatevikg1 Jan 12, 2026
988ff0c
PatternResolver registration
tatevikg1 Jan 12, 2026
6fa0137
UserTrackValueResolver
tatevikg1 Jan 12, 2026
026fedb
NormalizePlaceholderKey
tatevikg1 Jan 13, 2026
6823f07
UserDataSupportingResolver
tatevikg1 Jan 13, 2026
1a83a77
UserPersonalizer for SubscriptionConfirmationMessageHandler
tatevikg1 Jan 13, 2026
cbdb3b2
supportingResolvers
tatevikg1 Jan 13, 2026
9ac9cae
EmailBuilder
tatevikg1 Jan 14, 2026
10e5fa5
MailConstructorInterface
tatevikg1 Jan 14, 2026
51d5542
FooterValueResolver
tatevikg1 Jan 14, 2026
6a2ae3c
applyCampaignHeaders
tatevikg1 Jan 14, 2026
86f02a2
Add missing configs
tatevikg1 Jan 15, 2026
93e95e4
AttachmentAdder
tatevikg1 Jan 15, 2026
fc63fa4
applyContentAndFormatting
tatevikg1 Jan 16, 2026
89ba6df
Autowire, autoregister
tatevikg1 Jan 16, 2026
5fd4e4a
applyContentAndFormatting
tatevikg1 Jan 16, 2026
c121aa2
separate base email builder
tatevikg1 Jan 19, 2026
244a545
Tests
tatevikg1 Jan 19, 2026
e34d359
Style
tatevikg1 Jan 19, 2026
3209e63
Ref
tatevikg1 Jan 19, 2026
507e2a3
return
tatevikg1 Jan 20, 2026
8e22349
Ref
tatevikg1 Jan 20, 2026
5ce44d6
Style
tatevikg1 Jan 20, 2026
5c65671
Ref: increment sentAs columns
tatevikg1 Jan 20, 2026
8647fcf
After review 0
tatevikg1 Jan 21, 2026
76f6665
Add tests
tatevikg1 Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/parameters.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ parameters:
env(APP_DEV_EMAIL): 'dev@dev.com'
app.powered_by_phplist: '%%env(APP_POWERED_BY_PHPLIST)%%'
env(APP_POWERED_BY_PHPLIST): '0'
app.preference_page_show_private_lists: '%%env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS)%%'
env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS): '0'

# Email configuration
app.mailer_from: '%%env(MAILER_FROM)%%'
Expand Down
2 changes: 2 additions & 0 deletions config/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ services:
PhpList\Core\Domain\Configuration\Service\UserPersonalizer:
autowire: true
autoconfigure: true
arguments:
$preferencePageShowPrivateLists: '%app.preference_page_show_private_lists%'
Comment thread
TatevikGr marked this conversation as resolved.
Outdated

PhpList\Core\Domain\Configuration\Service\LegacyUrlBuilder:
autowire: true
Expand Down
11 changes: 11 additions & 0 deletions src/Domain/Configuration/Model/OutputFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace PhpList\Core\Domain\Configuration\Model;

enum OutputFormat: string
{
case Html = 'html';
case Text = 'text';
}
76 changes: 68 additions & 8 deletions src/Domain/Configuration/Service/UserPersonalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
namespace PhpList\Core\Domain\Configuration\Service;

use PhpList\Core\Domain\Configuration\Model\ConfigOption;
use PhpList\Core\Domain\Configuration\Model\OutputFormat;
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository;
use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository;
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
use PhpList\Core\Domain\Subscription\Service\Resolver\AttributeValueResolver;
use Symfony\Contracts\Translation\TranslatorInterface;

class UserPersonalizer
{
Expand All @@ -19,11 +22,14 @@ public function __construct(
private readonly LegacyUrlBuilder $urlBuilder,
private readonly SubscriberRepository $subscriberRepository,
private readonly SubscriberAttributeValueRepository $attributesRepository,
private readonly AttributeValueResolver $attributeValueResolver
private readonly AttributeValueResolver $attributeValueResolver,
private readonly SubscriberListRepository $subscriberListRepository,
private readonly TranslatorInterface $translator,
private readonly bool $preferencePageShowPrivateLists = false
) {
}

public function personalize(string $value, string $email): string
public function personalize(string $value, string $email, OutputFormat $format): string
{
$user = $this->subscriberRepository->findOneByEmail($email);
if (!$user) {
Expand All @@ -33,18 +39,49 @@ public function personalize(string $value, string $email): string
$resolver = new PlaceholderResolver();
$resolver->register('EMAIL', fn() => $user->getEmail());

$resolver->register('UNSUBSCRIBEURL', function () use ($user) {
$resolver->register('UNSUBSCRIBEURL', function () use ($user, $format) {
$base = $this->config->getValue(ConfigOption::UnsubscribeUrl) ?? '';
return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE;
$url = $this->urlBuilder->withUid($base, $user->getUniqueId());

if ($format === OutputFormat::Html) {
$label = $this->translator->trans('Unsubscribe');
$safeUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$safeLabel = htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

return '<a href="' . $safeUrl . '">' . $safeLabel . '</a>' . self::PHP_SPACE;
}

return $url . self::PHP_SPACE;
});

$resolver->register('CONFIRMATIONURL', function () use ($user) {
$resolver->register('CONFIRMATIONURL', function () use ($user, $format) {
$base = $this->config->getValue(ConfigOption::ConfirmationUrl) ?? '';
return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE;
$url = $this->urlBuilder->withUid($base, $user->getUniqueId());

if ($format === OutputFormat::Html) {
$label = $this->translator->trans('Confirm');
$safeUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$safeLabel = htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

return '<a href="' . $safeUrl . '">' . $safeLabel . '</a>' . self::PHP_SPACE;
}

return $url . self::PHP_SPACE;
});
$resolver->register('PREFERENCESURL', function () use ($user) {

$resolver->register('PREFERENCESURL', function () use ($user, $format) {
$base = $this->config->getValue(ConfigOption::PreferencesUrl) ?? '';
return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE;
$url = $this->urlBuilder->withUid($base, $user->getUniqueId());

if ($format === OutputFormat::Html) {
$label = $this->translator->trans('Update preferences');
$safeUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$safeLabel = htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

return '<a href="' . $safeUrl . '">' . $safeLabel . '</a>' . self::PHP_SPACE;
}

return $url . self::PHP_SPACE;
});

$resolver->register(
Expand All @@ -54,6 +91,29 @@ public function personalize(string $value, string $email): string
$resolver->register('DOMAIN', fn() => $this->config->getValue(ConfigOption::Domain) ?? '');
$resolver->register('WEBSITE', fn() => $this->config->getValue(ConfigOption::Website) ?? '');

$resolver->register('LISTS', function () use ($user, $format) {
$names = $this->subscriberListRepository->getActiveListNamesForSubscriber(
subscriber: $user,
showPrivate: $this->preferencePageShowPrivateLists
);

if ($names === []) {
return $this->translator
->trans('Sorry, you are not subscribed to any of our newsletters with this email address.');
}

$separator = $format === OutputFormat::Html ? '<br/>' : "\n";

if ($format === OutputFormat::Html) {
$names = array_map(
static fn(string $name) => htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
$names
);
}

return implode($separator, $names);
});

$userAttributes = $this->attributesRepository->getForSubscriber($user);
foreach ($userAttributes as $userAttribute) {
$resolver->register(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@
#[AsMessageHandler]
class AsyncEmailMessageHandler
{
private EmailService $emailService;

public function __construct(EmailService $emailService)
public function __construct(private readonly EmailService $emailService)
{
$this->emailService = $emailService;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use DateTimeImmutable;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use PhpList\Core\Domain\Configuration\Model\OutputFormat;
use PhpList\Core\Domain\Configuration\Service\UserPersonalizer;
use PhpList\Core\Domain\Messaging\Exception\MessageCacheMissingException;
use PhpList\Core\Domain\Messaging\Exception\MessageSizeLimitExceededException;
Expand Down Expand Up @@ -195,19 +196,29 @@ private function handleEmailSending(
MessagePrecacheDto $precachedContent,
): void {
$processed = $this->messagePreparator->processMessageLinks(
$campaign->getId(),
$precachedContent,
$subscriber
campaignId: $campaign->getId(),
cachedMessageDto: $precachedContent,
subscriber: $subscriber
);
$processed->textContent = $this->userPersonalizer->personalize(
$processed->textContent,
$subscriber->getEmail(),
value: $processed->textContent,
email: $subscriber->getEmail(),
format:OutputFormat::Text,
);
$processed->footer = $this->userPersonalizer->personalize(
value: $processed->footer,
email: $subscriber->getEmail(),
format: OutputFormat::Text,
);
$processed->footer = $this->userPersonalizer->personalize($processed->footer, $subscriber->getEmail());

try {
$email = $this->rateLimitedCampaignMailer->composeEmail($campaign, $subscriber, $processed);
$this->mailer->send($email);
$email = $this->emailBuilder->buildPhplistEmail(
$campaign->getId(),
$subscriber->getEmail(),
$processed->subject,
$processed->textContent,
);
$this->rateLimitedCampaignMailer->send($email);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
($this->mailSizeChecker)($campaign, $email, $subscriber->hasHtmlEmail());
$this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
} catch (MessageSizeLimitExceededException $e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@
#[AsMessageHandler]
class PasswordResetMessageHandler
{
private EmailService $emailService;
private TranslatorInterface $translator;
private string $passwordResetUrl;

public function __construct(EmailService $emailService, TranslatorInterface $translator, string $passwordResetUrl)
{
$this->emailService = $emailService;
$this->translator = $translator;
$this->passwordResetUrl = $passwordResetUrl;
public function __construct(
private readonly EmailService $emailService,
private readonly TranslatorInterface $translator,
private readonly string $passwordResetUrl
) {
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,11 @@
#[AsMessageHandler]
class SubscriberConfirmationMessageHandler
{
private EmailService $emailService;
private TranslatorInterface $translator;
private string $confirmationUrl;

public function __construct(EmailService $emailService, TranslatorInterface $translator, string $confirmationUrl)
{
$this->emailService = $emailService;
$this->translator = $translator;
$this->confirmationUrl = $confirmationUrl;
public function __construct(
private readonly EmailService $emailService,
private readonly TranslatorInterface $translator,
private readonly string $confirmationUrl
) {
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace PhpList\Core\Domain\Messaging\MessageHandler;

use PhpList\Core\Domain\Configuration\Model\ConfigOption;
use PhpList\Core\Domain\Configuration\Model\OutputFormat;
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
use PhpList\Core\Domain\Configuration\Service\UserPersonalizer;
use PhpList\Core\Domain\Messaging\Message\SubscriptionConfirmationMessage;
Expand All @@ -20,24 +21,13 @@
#[AsMessageHandler]
class SubscriptionConfirmationMessageHandler
{
private EmailService $emailService;
private ConfigProvider $configProvider;
private LoggerInterface $logger;
private UserPersonalizer $userPersonalizer;
private SubscriberListRepository $subscriberListRepository;

public function __construct(
EmailService $emailService,
ConfigProvider $configProvider,
LoggerInterface $logger,
UserPersonalizer $userPersonalizer,
SubscriberListRepository $subscriberListRepository,
private readonly EmailService $emailService,
private readonly ConfigProvider $configProvider,
private readonly LoggerInterface $logger,
private readonly UserPersonalizer $userPersonalizer,
private readonly SubscriberListRepository $subscriberListRepository,
) {
$this->emailService = $emailService;
$this->configProvider = $configProvider;
$this->logger = $logger;
$this->userPersonalizer = $userPersonalizer;
$this->subscriberListRepository = $subscriberListRepository;
}

/**
Expand All @@ -47,14 +37,19 @@ public function __invoke(SubscriptionConfirmationMessage $message): void
{
$subject = $this->configProvider->getValue(ConfigOption::SubscribeEmailSubject);
$textContent = $this->configProvider->getValue(ConfigOption::SubscribeMessage);
$personalizedTextContent = $this->userPersonalizer->personalize($textContent, $message->getUniqueId());
$listOfLists = $this->getListNames($message->getListIds());
$replacedTextContent = str_replace('[LISTS]', $listOfLists, $personalizedTextContent);
$replacedTextContent = str_replace('[LISTS]', $listOfLists, $textContent);

$personalizedTextContent = $this->userPersonalizer->personalize(
value: $replacedTextContent,
email: $message->getUniqueId(),
format: OutputFormat::Text,
);
Comment thread
TatevikGr marked this conversation as resolved.
Outdated
Comment thread
TatevikGr marked this conversation as resolved.

$email = (new Email())
->to($message->getEmail())
->subject($subject)
->text($replacedTextContent);
->text($personalizedTextContent);

$this->emailService->sendEmail($email);

Expand All @@ -63,14 +58,6 @@ public function __invoke(SubscriptionConfirmationMessage $message): void

private function getListNames(array $listIds): string
{
$listNames = [];
foreach ($listIds as $id) {
$list = $this->subscriberListRepository->find($id);
if ($list) {
$listNames[] = $list->getName();
}
}

return implode(', ', $listNames);
return implode(', ', $this->subscriberListRepository->getListNames($listIds));
}
}
48 changes: 25 additions & 23 deletions src/Domain/Messaging/Service/MessagePrecacheService.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,8 @@ public function precacheMessage(Message $campaign, $loadedMessageData, ?bool $fo
//# but that has quite some impact on speed. So check if that's the case and apply
$messagePrecacheDto->userSpecificUrl = preg_match('/\[.+\]/', $loadedMessageData['sendurl']);

if (!$messagePrecacheDto->userSpecificUrl) {
if (!$this->applyRemoteContentIfPresent($messagePrecacheDto, $loadedMessageData)) {
return false;
}
if (!$this->applyRemoteContentIfPresent($messagePrecacheDto, $loadedMessageData)) {
return false;
}

$messagePrecacheDto->googleTrack = $loadedMessageData['google_track'];
Expand Down Expand Up @@ -181,27 +179,31 @@ private function applyTemplate(MessagePrecacheDto $messagePrecacheDto, $loadedMe

private function applyRemoteContentIfPresent(MessagePrecacheDto $messagePrecacheDto, $loadedMessageData): bool
{
if (preg_match('/\[URL:([^\s]+)\]/i', $messagePrecacheDto->content, $regs)) {
$remoteContent = ($this->remotePageFetcher)($regs[1], []);

if ($remoteContent) {
$messagePrecacheDto->content = str_replace($regs[0], $remoteContent, $messagePrecacheDto->content);
$messagePrecacheDto->htmlFormatted = $this->isHtml($remoteContent);

//# 17086 - disregard any template settings when we have a valid remote URL
$messagePrecacheDto->template = null;
$messagePrecacheDto->templateText = null;
$messagePrecacheDto->templateId = null;
} else {
$this->eventLogManager->log(
page: 'unknown page',
entry: 'Error fetching URL: '.$loadedMessageData['sendurl'].' cannot proceed',
);

return false;
}
if (
$messagePrecacheDto->userSpecificUrl
|| !preg_match('/\[URL:([^\s]+)\]/i', $messagePrecacheDto->content, $regs)
) {
return true;
}

$remoteContent = ($this->remotePageFetcher)($regs[1], []);
if (!$remoteContent) {
$this->eventLogManager->log(
page: 'unknown page',
entry: 'Error fetching URL: ' . $loadedMessageData['sendurl'] . ' cannot proceed',
);

return false;
}

$messagePrecacheDto->content = str_replace($regs[0], $remoteContent, $messagePrecacheDto->content);
$messagePrecacheDto->htmlFormatted = $this->isHtml($remoteContent);

//# 17086 - disregard any template settings when we have a valid remote URL
$messagePrecacheDto->template = null;
$messagePrecacheDto->templateText = null;
$messagePrecacheDto->templateId = null;

return true;
Comment thread
TatevikGr marked this conversation as resolved.
Outdated
}

Expand Down
Loading
Loading