Skip to content

Commit 6a99463

Browse files
committed
Email with messageHandler
1 parent 46a19fc commit 6a99463

File tree

13 files changed

+295
-53
lines changed

13 files changed

+295
-53
lines changed

config/parameters.yml.dist

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ parameters:
2828
app.mailer_dsn: '%%env(MAILER_DSN)%%'
2929
env(MAILER_DSN): 'null://null'
3030
app.confirmation_url: '%%env(CONFIRMATION_URL)%%'
31-
env(CONFIRMATION_URL): 'https://example.com/confirm/'
31+
env(CONFIRMATION_URL): 'https://example.com/subscriber/confirm/'
32+
app.subscription_confirmation_url: '%%env(SUBSCRIPTION_CONFIRMATION_URL)%%'
33+
env(SUBSCRIPTION_CONFIRMATION_URL): 'https://example.com/subscription/confirm/'
3234
app.password_reset_url: '%%env(PASSWORD_RESET_URL)%%'
3335
env(PASSWORD_RESET_URL): 'https://example.com/reset/'
3436

config/services/messenger.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ services:
2222
tags: [ 'messenger.message_handler' ]
2323
arguments:
2424
$passwordResetUrl: '%app.password_reset_url%'
25+
26+
PhpList\Core\Domain\Messaging\MessageHandler\SubscriptionConfirmationMessageHandler:
27+
autowire: true
28+
autoconfigure: true
29+
tags: [ 'messenger.message_handler' ]
30+

config/services/services.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,15 @@ services:
120120
arguments:
121121
$maxSeconds: '%messaging.max_process_time%'
122122

123-
124123
PhpList\Core\Domain\Identity\Service\PermissionChecker:
125124
autowire: true
126125
autoconfigure: true
127126
public: true
127+
128+
PhpList\Core\Domain\Configuration\Service\UserPersonalizer:
129+
autowire: true
130+
autoconfigure: true
131+
132+
PhpList\Core\Domain\Configuration\Service\LegacyUrlBuilder:
133+
autowire: true
134+
autoconfigure: true
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Configuration\Service;
6+
7+
class LegacyUrlBuilder
8+
{
9+
public function withUid(string $baseUrl, string $uid): string
10+
{
11+
$parts = parse_url($baseUrl) ?: [];
12+
$query = [];
13+
if (!empty($parts['query'])) {
14+
parse_str($parts['query'], $query);
15+
}
16+
$query['uid'] = $uid;
17+
18+
$parts['query'] = http_build_query($query);
19+
20+
// rebuild url
21+
$scheme = $parts['scheme'] ?? 'https';
22+
$host = $parts['host'] ?? '';
23+
$port = isset($parts['port']) ? ':'.$parts['port'] : '';
24+
$path = $parts['path'] ?? '';
25+
$queryStr = $parts['query'] ? '?'.$parts['query'] : '';
26+
$frag = isset($parts['fragment']) ? '#'.$parts['fragment'] : '';
27+
28+
return "{$scheme}://{$host}{$port}{$path}{$queryStr}{$frag}";
29+
}
30+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Configuration\Service;
6+
7+
class PlaceholderResolver
8+
{
9+
/** @var array<string, callable():string> */
10+
private array $providers = [];
11+
12+
public function register(string $token, callable $provider): void
13+
{
14+
// tokens like [UNSUBSCRIBEURL] (case-insensitive)
15+
$this->providers[strtoupper($token)] = $provider;
16+
}
17+
18+
public function resolve(?string $input): ?string
19+
{
20+
if ($input === null || $input === '') return $input;
21+
22+
// Replace [TOKEN] (case-insensitive)
23+
return preg_replace_callback('/\[(\w+)\]/i', function ($m) {
24+
$key = strtoupper($m[1]);
25+
if (!isset($this->providers[$key])) {
26+
return $m[0];
27+
}
28+
return (string) ($this->providers[$key])();
29+
}, $input);
30+
}
31+
}

src/Domain/Configuration/Service/Provider/DefaultConfigProvider.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -224,49 +224,49 @@ private static function init(): void
224224
'category' => 'transactional',
225225
],
226226
'subscribeurl' => [
227-
'value' => $publicSchema."://[WEBSITE]$pageRoot/subscribe",
227+
'value' => $publicSchema."://[WEBSITE]$pageRoot/?p=subscribe",
228228
'description' => self::$translator->trans('URL where subscribers can sign up'),
229229
'type' => 'url',
230230
'allowempty' => 0,
231231
'category' => 'subscription',
232232
],
233233
'unsubscribeurl' => [
234-
'value' => $publicSchema."://[WEBSITE]$pageRoot/unsubscribe",
234+
'value' => $publicSchema."://[WEBSITE]$pageRoot/?p=unsubscribe",
235235
'description' => self::$translator->trans('URL where subscribers can unsubscribe'),
236236
'type' => 'url',
237237
'allowempty' => 0,
238238
'category' => 'subscription',
239239
],
240240
'blacklisturl' => [
241-
'value' => $publicSchema."://[WEBSITE]$pageRoot/donotsend",
241+
'value' => $publicSchema."://[WEBSITE]$pageRoot/?p=donotsend",
242242
'description' => self::$translator->trans('URL where unknown users can unsubscribe (do-not-send-list)'),
243243
'type' => 'url',
244244
'allowempty' => 0,
245245
'category' => 'subscription',
246246
],
247247
'confirmationurl' => [
248-
'value' => $publicSchema."://[WEBSITE]$pageRoot/confirm",
248+
'value' => $publicSchema."://[WEBSITE]$pageRoot/?p=confirm",
249249
'description' => self::$translator->trans('URL where subscribers have to confirm their subscription'),
250250
'type' => 'text',
251251
'allowempty' => 0,
252252
'category' => 'subscription',
253253
],
254254
'preferencesurl' => [
255-
'value' => $publicSchema."://[WEBSITE]$pageRoot/preferences",
255+
'value' => $publicSchema."://[WEBSITE]$pageRoot/?p=preferences",
256256
'description' => self::$translator->trans('URL where subscribers can update their details'),
257257
'type' => 'text',
258258
'allowempty' => 0,
259259
'category' => 'subscription',
260260
],
261261
'forwardurl' => [
262-
'value' => $publicSchema."://[WEBSITE]$pageRoot/forward",
262+
'value' => $publicSchema."://[WEBSITE]$pageRoot/?p=forward",
263263
'description' => self::$translator->trans('URL for forwarding messages'),
264264
'type' => 'text',
265265
'allowempty' => 0,
266266
'category' => 'subscription',
267267
],
268268
'vcardurl' => [
269-
'value' => $publicSchema."://[WEBSITE]$pageRoot/vcard",
269+
'value' => $publicSchema."://[WEBSITE]$pageRoot/?p=vcard",
270270
'description' => self::$translator->trans('URL for downloading vcf card'),
271271
'type' => 'text',
272272
'allowempty' => 0,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Configuration\Service;
6+
7+
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
8+
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
9+
use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository;
10+
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
11+
use PhpList\Core\Domain\Subscription\Service\Resolver\AttributeValueResolver;
12+
13+
class UserPersonalizer
14+
{
15+
private const PHP_SPACE = ' ';
16+
17+
public function __construct(
18+
private readonly ConfigProvider $config,
19+
private readonly LegacyUrlBuilder $urlBuilder,
20+
private readonly SubscriberRepository $subscriberRepository,
21+
private readonly SubscriberAttributeValueRepository $attributesRepository,
22+
private readonly AttributeValueResolver $attributeValueResolver
23+
) {}
24+
25+
public function personalize(string $value, string $email): string
26+
{
27+
$user = $this->subscriberRepository->findOneByEmail($email);
28+
if (!$user) {
29+
return $value;
30+
}
31+
32+
$resolver = new PlaceholderResolver();
33+
$resolver->register('EMAIL', fn() => $user->getEmail());
34+
35+
$resolver->register('UNSUBSCRIBEURL', function () use ($user) {
36+
$base = $this->config->getValue(ConfigOption::UnsubscribeUrl) ?? '';
37+
return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE;
38+
});
39+
40+
$resolver->register('CONFIRMATIONURL', function () use ($user) {
41+
$base = $this->config->getValue(ConfigOption::ConfirmationUrl) ?? '';
42+
return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE;
43+
});
44+
$resolver->register('PREFERENCESURL', function () use ($user) {
45+
$base = $this->config->getValue(ConfigOption::PreferencesUrl) ?? '';
46+
return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE;
47+
});
48+
49+
$resolver->register(
50+
'SUBSCRIBEURL',
51+
fn() => ($this->config->getValue(ConfigOption::SubscribeUrl) ?? '') . self::PHP_SPACE
52+
);
53+
$resolver->register('DOMAIN', fn() => $this->config->getValue(ConfigOption::Domain) ?? '');
54+
$resolver->register('WEBSITE', fn() => $this->config->getValue(ConfigOption::Website) ?? '');
55+
56+
$userAttributes = $this->attributesRepository->getForSubscriber($user);
57+
foreach ($userAttributes as $userAttribute) {
58+
$resolver->register(
59+
strtoupper($userAttribute->getAttributeDefinition()->getName()),
60+
fn() => $this->attributeValueResolver->resolve($userAttribute)
61+
);
62+
}
63+
64+
$out = $resolver->resolve($value);
65+
66+
return (string) $out;
67+
}
68+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\Message;
6+
7+
/**
8+
* Message class for asynchronous subscriber confirmation email processing
9+
*/
10+
class SubscriptionConfirmationMessage
11+
{
12+
private string $email;
13+
private string $uniqueId;
14+
private array $listIds;
15+
private bool $htmlEmail;
16+
17+
/**
18+
* @SuppressWarnings("BooleanArgumentFlag")
19+
*/
20+
public function __construct(
21+
string $email,
22+
string $uniqueId,
23+
array $listIds,
24+
bool $htmlEmail = false
25+
) {
26+
$this->email = $email;
27+
$this->uniqueId = $uniqueId;
28+
$this->listIds = $listIds;
29+
$this->htmlEmail = $htmlEmail;
30+
}
31+
32+
public function getEmail(): string
33+
{
34+
return $this->email;
35+
}
36+
37+
public function getUniqueId(): string
38+
{
39+
return $this->uniqueId;
40+
}
41+
42+
public function getListIds(): array
43+
{
44+
return $this->listIds;
45+
}
46+
47+
public function hasHtmlEmail(): bool
48+
{
49+
return $this->htmlEmail;
50+
}
51+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\MessageHandler;
6+
7+
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
8+
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
9+
use PhpList\Core\Domain\Configuration\Service\UserPersonalizer;
10+
use PhpList\Core\Domain\Messaging\Message\SubscriberConfirmationMessage;
11+
use PhpList\Core\Domain\Messaging\Service\EmailService;
12+
use Psr\Log\LoggerInterface;
13+
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
14+
use Symfony\Component\Mime\Email;
15+
16+
/**
17+
* Handler for processing asynchronous subscription confirmation email messages
18+
*/
19+
#[AsMessageHandler]
20+
class SubscriptionConfirmationMessageHandler
21+
{
22+
private EmailService $emailService;
23+
private ConfigProvider $configProvider;
24+
private LoggerInterface $logger;
25+
private UserPersonalizer $userPersonalizer;
26+
27+
public function __construct(
28+
EmailService $emailService,
29+
ConfigProvider $configProvider,
30+
LoggerInterface $logger,
31+
UserPersonalizer $userPersonalizer,
32+
) {
33+
$this->emailService = $emailService;
34+
$this->configProvider = $configProvider;
35+
$this->logger = $logger;
36+
$this->userPersonalizer = $userPersonalizer;
37+
}
38+
39+
/**
40+
* Process a subscription confirmation message by sending the confirmation email
41+
*/
42+
public function __invoke(SubscriberConfirmationMessage $message): void
43+
{
44+
$subject = $this->configProvider->getValue(ConfigOption::SubscribeEmailSubject);
45+
$textContent = $this->configProvider->getValue(ConfigOption::SubscribeMessage);
46+
$replacedTextContent = $this->userPersonalizer->personalize($textContent, $message->getUniqueId());
47+
48+
$email = (new Email())
49+
->to($message->getEmail())
50+
->subject($subject)
51+
->text($replacedTextContent);
52+
53+
$this->emailService->sendEmail($email);
54+
55+
$this->logger->info('Subscription confirmation email sent to {email}', ['email' => $message->getEmail()]);
56+
}
57+
}

src/Domain/Subscription/Repository/SubscriberAttributeValueRepository.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,16 @@ public function getFilteredAfterId(int $lastId, int $limit, ?FilterRequestInterf
6464
->getQuery()
6565
->getResult();
6666
}
67+
68+
/** @return SubscriberAttributeValue[] */
69+
public function getForSubscriber(Subscriber $subscriber): array
70+
{
71+
return $this->createQueryBuilder('sa')
72+
->join('sa.subscriber', 's')
73+
->join('sa.attributeDefinition', 'ad')
74+
->where('s = :subscriber')
75+
->setParameter('subscriber', $subscriber)
76+
->getQuery()
77+
->getResult();
78+
}
6779
}

0 commit comments

Comments
 (0)