Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 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
8 changes: 8 additions & 0 deletions config/parameters.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ 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'
app.rest_api_domain: '%%env(REST_API_DOMAIN)%%'
env(REST_API_DOMAIN): 'https://example.com/api/v2'

# Email configuration
app.mailer_from: '%%env(MAILER_FROM)%%'
Expand Down Expand Up @@ -115,6 +119,10 @@ parameters:
env(EXTERNALIMAGE_TIMEOUT): '30'
messaging.external_image_max_size: '%%env(EXTERNALIMAGE_MAXSIZE)%%'
env(EXTERNALIMAGE_MAXSIZE): '204800'
messaging.forward_alternative_content: '%%env(FORWARD_ALTERNATIVE_CONTENT)%%'
env(FORWARD_ALTERNATIVE_CONTENT): '0'
messaging.email_text_credits: '%%env(EMAILTEXTCREDITS)%%'
env(EMAILTEXTCREDITS): '0'

phplist.upload_images_dir: '%%env(PHPLIST_UPLOADIMAGES_DIR)%%'
env(PHPLIST_UPLOADIMAGES_DIR): 'images'
Expand Down
25 changes: 25 additions & 0 deletions config/services/builders.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,28 @@ services:
PhpList\Core\Domain\Messaging\Service\Builder\MessageOptionsBuilder:
autowire: true
autoconfigure: true

# Concrete mail constructors
PhpList\Core\Domain\Messaging\Service\Constructor\SystemMailContentBuilder: ~
PhpList\Core\Domain\Messaging\Service\Constructor\CampaignMailContentBuilder: ~

# Two EmailBuilder services with different constructors injected
Core.EmailBuilder.system:
class: PhpList\Core\Domain\Messaging\Service\Builder\EmailBuilder
arguments:
$mailConstructor: '@PhpList\Core\Domain\Messaging\Service\Constructor\SystemMailContentBuilder'
$googleSenderId: '%messaging.google_sender_id%'
$useAmazonSes: '%messaging.use_amazon_ses%'
$usePrecedenceHeader: '%messaging.use_precedence_header%'
$devVersion: '%app.dev_version%'
$devEmail: '%app.dev_email%'

Core.EmailBuilder.campaign:
class: PhpList\Core\Domain\Messaging\Service\Builder\EmailBuilder
arguments:
$mailConstructor: '@PhpList\Core\Domain\Messaging\Service\Constructor\CampaignMailContentBuilder'
$googleSenderId: '%messaging.google_sender_id%'
$useAmazonSes: '%messaging.use_amazon_ses%'
$usePrecedenceHeader: '%messaging.use_precedence_header%'
$devVersion: '%app.dev_version%'
$devEmail: '%app.dev_email%'
5 changes: 5 additions & 0 deletions config/services/messenger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ services:

PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler:
autowire: true
autoconfigure: true
arguments:
$campaignEmailBuilder: '@Core.EmailBuilder.campaign'
$systemEmailBuilder: '@Core.EmailBuilder.system'
$messageEnvelope: '%app.config.message_from_address%'

PhpList\Core\Domain\Subscription\MessageHandler\DynamicTableMessageHandler:
autowire: true
Expand Down
5 changes: 5 additions & 0 deletions config/services/parameters.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
parameters:
# Flattened parameters for direct DI usage (Symfony does not support dot access into arrays)
app.config.message_from_address: 'news@example.com'
app.config.default_message_age: 15768000

# Keep original grouped array for legacy/config-provider usage
app.config:
message_from_address: 'news@example.com'
admin_address: 'admin@example.com'
Expand Down
5 changes: 5 additions & 0 deletions config/services/repositories.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ services:
arguments:
- PhpList\Core\Domain\Configuration\Model\EventLog

PhpList\Core\Domain\Configuration\Repository\UrlCacheRepository:
parent: PhpList\Core\Domain\Common\Repository\AbstractRepository
arguments:
- PhpList\Core\Domain\Configuration\Model\UrlCache


PhpList\Core\Domain\Identity\Repository\AdministratorRepository:
parent: PhpList\Core\Domain\Common\Repository\AbstractRepository
Expand Down
6 changes: 6 additions & 0 deletions config/services/resolvers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ services:
PhpList\Core\Bounce\Service\BounceActionResolver:
arguments:
- !tagged_iterator { tag: 'phplist.bounce_action_handler' }

_instanceof:
PhpList\Core\Domain\Configuration\Service\Placeholder\PlaceholderValueResolverInterface:
tags: ['phplist.placeholder_resolver']
PhpList\Core\Domain\Configuration\Service\Placeholder\PatternValueResolverInterface:
tags: [ 'phplist.pattern_resolver' ]
63 changes: 62 additions & 1 deletion config/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,39 @@ services:
autowire: true
autoconfigure: true

# Html to Text converter used by mail constructors
PhpList\Core\Domain\Common\Html2Text:
autowire: true
autoconfigure: true

# Rewrites relative asset URLs in fetched HTML to absolute ones
PhpList\Core\Domain\Common\HtmlUrlRewriter:
autowire: true
autoconfigure: true

# External image caching/downloading helper used by TemplateImageEmbedder
PhpList\Core\Domain\Common\ExternalImageService:
autowire: true
autoconfigure: true
arguments:
$tempDir: '%kernel.cache_dir%'
# Use literal defaults if parameters are not defined in this environment
$externalImageMaxAge: 0
$externalImageMaxSize: 204800
$externalImageTimeout: 30

# Embed images from templates and filesystem into HTML emails
PhpList\Core\Domain\Messaging\Service\TemplateImageEmbedder:
autowire: true
autoconfigure: true
arguments:
$documentRoot: '%kernel.project_dir%/public'
# Reuse upload_images_dir for editorImagesDir if a dedicated parameter is absent
$editorImagesDir: '%phplist.upload_images_dir%'
$embedExternalImages: '%messaging.embed_external_images%'
$embedUploadedImages: '%messaging.embed_uploaded_images%'
$uploadImagesDir: '%phplist.upload_images_dir%'

PhpList\Core\Domain\Messaging\Service\RateLimitedCampaignMailer:
autowire: true
autoconfigure: true
Expand Down Expand Up @@ -120,9 +153,12 @@ services:
autoconfigure: true
public: true

PhpList\Core\Domain\Configuration\Service\UserPersonalizer:
PhpList\Core\Domain\Configuration\Service\MessagePlaceholderProcessor:
autowire: true
autoconfigure: true
arguments:
$placeholderResolvers: !tagged_iterator phplist.placeholder_resolver
$patternResolvers: !tagged_iterator phplist.pattern_resolver

PhpList\Core\Domain\Configuration\Service\LegacyUrlBuilder:
autowire: true
Expand All @@ -139,3 +175,28 @@ services:
autoconfigure: true
arguments:
$maxMailSize: '%messaging.max_mail_size%'

# Loads and normalises message data for campaigns
PhpList\Core\Domain\Messaging\Service\MessageDataLoader:
autowire: true
autoconfigure: true
arguments:
$defaultMessageAge: '%app.config.default_message_age%'
Comment thread
TatevikGr marked this conversation as resolved.

# Common helpers required by precache/message building
PhpList\Core\Domain\Common\TextParser:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Common\RemotePageFetcher:
autowire: true
autoconfigure: true

# Pre-caches base message content (HTML/Text/template) for campaigns
PhpList\Core\Domain\Messaging\Service\MessagePrecacheService:
autowire: true
autoconfigure: true
arguments:
$useManualTextPart: '%messaging.use_manual_text_part%'
$uploadImageDir: '%phplist.upload_images_dir%'
$publicSchema: '%phplist.public_schema%'
20 changes: 20 additions & 0 deletions resources/translations/messages.en.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,26 @@ Thank you.</target>
<source>phplist has started sending the campaign with subject %subject%</source>
<target>__phplist has started sending the campaign with subject %subject%</target>
</trans-unit>
<trans-unit id="PpLvt2Z" resname="Unsubscribe">
<source>Unsubscribe</source>
<target>__Unsubscribe</target>
</trans-unit>
<trans-unit id="njfOwly" resname="This link">
<source>This link</source>
<target>__This link</target>
</trans-unit>
<trans-unit id="7r3SSnf" resname="Confirm">
<source>Confirm</source>
<target>__Confirm</target>
</trans-unit>
<trans-unit id="YibbDEd" resname="Update preferences">
<source>Update preferences</source>
<target>__Update preferences</target>
</trans-unit>
<trans-unit id="BkWF7jw" resname="Sorry, you are not subscribed to any of our newsletters with this email address.">
<source>Sorry, you are not subscribed to any of our newsletters with this email address.</source>
<target>__Sorry, you are not subscribed to any of our newsletters with this email address.</target>
</trans-unit>
Comment thread
TatevikGr marked this conversation as resolved.
</body>
</file>
</xliff>
6 changes: 3 additions & 3 deletions src/Domain/Common/RemotePageFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function __construct(
) {
}

public function __invoke(string $url, array $userData): string
public function __invoke(string $url, array $userData): ?string
{
$url = $this->prepareUrl($url, $userData);

Expand Down Expand Up @@ -78,7 +78,7 @@ public function __invoke(string $url, array $userData): string
return $content;
}

private function fetchUrlDirect(string $url): string
private function fetchUrlDirect(string $url): ?string
{
try {
$response = $this->httpClient->request('GET', $url, [
Expand All @@ -88,7 +88,7 @@ private function fetchUrlDirect(string $url): string

return $response->getContent(false);
} catch (Throwable $e) {
return '';
return null;
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/Domain/Configuration/Model/ConfigOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ enum ConfigOption: string
case SubscribeMessage = 'subscribemessage';
case SubscribeEmailSubject = 'subscribesubject';
case UnsubscribeUrl = 'unsubscribeurl';
case BlacklistUrl = 'blacklisturl';
case ForwardUrl = 'forwardurl';
case ConfirmationUrl = 'confirmationurl';
case PreferencesUrl = 'preferencesurl';
case SubscribeUrl = 'subscribeurl';
// todo: check where is this defined
case Domain = 'domain';
Comment thread
TatevikGr marked this conversation as resolved.
case Website = 'website';
case MessageFromAddress = 'message_from_address';
Expand All @@ -33,4 +36,7 @@ enum ConfigOption: string
case PoweredByText = 'PoweredByText';
case UploadImageRoot = 'uploadimageroot';
case PageRoot = 'pageroot';
case OrganisationName = 'organisation_name';
case VCardUrl = 'vcardurl';
case HtmlEmailStyle = 'html_email_style';
}
46 changes: 46 additions & 0 deletions src/Domain/Configuration/Model/Dto/PlaceholderContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace PhpList\Core\Domain\Configuration\Model\Dto;

use PhpList\Core\Domain\Configuration\Model\OutputFormat;
use PhpList\Core\Domain\Messaging\Model\Dto\MessagePrecacheDto;
use PhpList\Core\Domain\Subscription\Model\Subscriber;

final class PlaceholderContext
{
public function __construct(
public readonly Subscriber $user,
public readonly OutputFormat $format,
public readonly ?MessagePrecacheDto $messagePrecacheDto = null,
public readonly string $locale = 'en',
private readonly ?string $forwardedBy = null,
private readonly ?int $messageId = null,
) {}

public function isHtml(): bool
{
return $this->format === OutputFormat::Html;
}

public function isText(): bool
{
return $this->format === OutputFormat::Text;
}
Comment thread
TatevikGr marked this conversation as resolved.

public function forwardedBy(): ?string
{
return $this->forwardedBy;
}

public function messageId(): ?int
{
return $this->messageId;
}

public function getUser(): Subscriber
{
return $this->user;
}
}
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';
}
12 changes: 11 additions & 1 deletion src/Domain/Configuration/Service/LegacyUrlBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@
class LegacyUrlBuilder
{
public function withUid(string $baseUrl, string $uid): string
{
return $this->withQueryParam($baseUrl, 'uid', $uid);
}

public function withEmail(string $baseUrl, string $email): string
{
return $this->withQueryParam($baseUrl, 'email', $email);
}

private function withQueryParam(string $baseUrl, string $paramName, string $paramValue): string
{
$parts = parse_url($baseUrl) ?: [];
$query = [];
if (!empty($parts['query'])) {
parse_str($parts['query'], $query);
}
$query['uid'] = $uid;
$query[$paramName] = $paramValue;

$parts['query'] = http_build_query($query);

Expand Down
Loading
Loading