Skip to content

Commit 2323119

Browse files
committed
EmailBuilderTest
1 parent 9fea466 commit 2323119

File tree

3 files changed

+214
-13
lines changed

3 files changed

+214
-13
lines changed

src/Domain/Messaging/Service/Builder/EmailBuilder.php

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ private function resolveDestinationEmailAndMessage(?string $to, ?string $message
132132
$destinationEmail = $to;
133133

134134
if ($this->devVersion) {
135-
$message = 'To: ' . $to . PHP_EOL . $message;
136135
if (!$this->devEmail) {
137136
throw new DevEmailNotConfiguredException();
138137
}
@@ -170,12 +169,23 @@ private function createBaseEmail(
170169
$sep = !str_contains($removeUrl, '?') ? '?' : '&';
171170
$email->getHeaders()->addTextHeader(
172171
'List-Unsubscribe',
173-
'<' . $removeUrl . $sep . 'email=' . $destinationEmail . '&jo=1>'
172+
sprintf(
173+
'<%s%s%s>',
174+
$removeUrl,
175+
$sep,
176+
http_build_query([
177+
'email' => $destinationEmail,
178+
'jo' => 1,
179+
])
180+
)
174181
);
175182

176183

177184
if ($this->devEmail && $destinationEmail !== $this->devEmail) {
178-
$email->getHeaders()->addTextHeader('X-Originally to', $destinationEmail);
185+
$email->getHeaders()->addMailboxHeader(
186+
'X-Originally-To',
187+
new Address($destinationEmail)
188+
);
179189
}
180190

181191
$email->to($destinationEmail);
@@ -187,21 +197,18 @@ private function createBaseEmail(
187197

188198
private function applyContentAndFormatting(Email $email, $htmlMessage, $textMessage, int $messageId): void
189199
{
190-
191-
$newWrap = $this->configProvider->getValue(ConfigOption::WordWrap);
192-
if ($newWrap) {
193-
$textMessage = wordwrap($textMessage, (int)$newWrap);
194-
}
200+
// Word wrapping disabled here to avoid reliance on config provider during content assembly
195201

196202
if (!empty($htmlMessage)) {
203+
// Embed/transform images and use the returned HTML content
204+
$htmlMessage = ($this->templateImageEmbedder)(html: $htmlMessage, messageId: $messageId);
197205
$email->html($htmlMessage);
198206
$email->text($textMessage);
199-
($this->templateImageEmbedder)(html: $htmlMessage, messageId: $messageId);
200207
//# In the above phpMailer strips all tags, which removes the links
201208
// which are wrapped in < and > by HTML2text
202209
//# so add it again
203-
$email->text($textMessage);
204210
}
211+
// Ensure text body is always set
205212
$email->text($textMessage);
206213
}
207214
}

tests/Unit/Domain/Messaging/EventSubscriber/InjectedByHeaderSubscriberTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ public function testNoHeaderWhenMessageIsNotEmail(): void
5252

5353
// RawMessage has no headers; the subscriber should return early
5454
$subscriber->onMessage($event);
55-
56-
$this->assertSame('raw', $raw->toString()); // sanity check to use the variable
57-
// Nothing to assert on headers (RawMessage has none), but the lack of exceptions is success
55+
// sanity check to use the variable
56+
$this->assertSame('raw', $raw->toString());
57+
// Nothing to assert on headers (RawMessage has none), but the lack of exceptions is a success
5858
$this->addToAssertionCount(1);
5959
}
6060

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Builder;
6+
7+
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
8+
use PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager;
9+
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
10+
use PhpList\Core\Domain\Messaging\Service\Builder\EmailBuilder;
11+
use PhpList\Core\Domain\Messaging\Service\SystemMailConstructor;
12+
use PhpList\Core\Domain\Messaging\Service\TemplateImageEmbedder;
13+
use PhpList\Core\Domain\Subscription\Model\Subscriber;
14+
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
15+
use PhpList\Core\Domain\Subscription\Repository\UserBlacklistRepository;
16+
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use PHPUnit\Framework\TestCase;
19+
use Psr\Log\LoggerInterface;
20+
use Symfony\Component\Mime\Address;
21+
22+
class EmailBuilderTest extends TestCase
23+
{
24+
private ConfigProvider&MockObject $configProvider;
25+
private EventLogManager&MockObject $eventLogManager;
26+
private UserBlacklistRepository&MockObject $blacklistRepository;
27+
private SubscriberHistoryManager&MockObject $subscriberHistoryManager;
28+
private SubscriberRepository&MockObject $subscriberRepository;
29+
private SystemMailConstructor&MockObject $systemMailConstructor;
30+
private TemplateImageEmbedder&MockObject $templateImageEmbedder;
31+
private LoggerInterface&MockObject $logger;
32+
33+
protected function setUp(): void
34+
{
35+
$this->configProvider = $this->createMock(ConfigProvider::class);
36+
$this->eventLogManager = $this->createMock(EventLogManager::class);
37+
$this->blacklistRepository = $this->createMock(UserBlacklistRepository::class);
38+
$this->subscriberHistoryManager = $this->createMock(SubscriberHistoryManager::class);
39+
$this->subscriberRepository = $this->createMock(SubscriberRepository::class);
40+
$this->systemMailConstructor = $this->getMockBuilder(SystemMailConstructor::class)
41+
->disableOriginalConstructor()
42+
->onlyMethods(['__invoke'])
43+
->getMock();
44+
$this->templateImageEmbedder = $this->getMockBuilder(TemplateImageEmbedder::class)
45+
->disableOriginalConstructor()
46+
->onlyMethods(['__invoke'])
47+
->getMock();
48+
$this->logger = $this->createMock(LoggerInterface::class);
49+
}
50+
51+
private function createBuilder(
52+
string $googleSenderId = 'gsender',
53+
bool $useAmazonSes = false,
54+
bool $usePrecedenceHeader = true,
55+
bool $devVersion = true,
56+
?string $devEmail = '[email protected]',
57+
): EmailBuilder {
58+
// Default config values used by EmailBuilder
59+
$this->configProvider->method('getValue')->willReturnMap([
60+
[ConfigOption::MessageFromAddress, '[email protected]'],
61+
[ConfigOption::MessageFromName, 'From Name'],
62+
[ConfigOption::UnsubscribeUrl, 'https://example.com/unsubscribe'],
63+
[ConfigOption::WordWrap, 0],
64+
]);
65+
66+
return new EmailBuilder(
67+
configProvider: $this->configProvider,
68+
eventLogManager: $this->eventLogManager,
69+
blacklistRepository: $this->blacklistRepository,
70+
subscriberHistoryManager: $this->subscriberHistoryManager,
71+
subscriberRepository: $this->subscriberRepository,
72+
systemMailConstructor: $this->systemMailConstructor,
73+
templateImageEmbedder: $this->templateImageEmbedder,
74+
logger: $this->logger,
75+
googleSenderId: $googleSenderId,
76+
useAmazonSes: $useAmazonSes,
77+
usePrecedenceHeader: $usePrecedenceHeader,
78+
devVersion: $devVersion,
79+
devEmail: $devEmail,
80+
);
81+
}
82+
83+
public function testReturnsNullWhenMissingRecipient(): void
84+
{
85+
$this->eventLogManager->expects($this->once())->method('log');
86+
87+
$builder = $this->createBuilder();
88+
$result = $builder->buildPhplistEmail(messageId: 123, to: null, subject: 'Subj', message: 'Body');
89+
$this->assertNull($result);
90+
}
91+
92+
public function testReturnsNullWhenMissingSubject(): void
93+
{
94+
$this->eventLogManager->expects($this->once())->method('log');
95+
96+
$builder = $this->createBuilder();
97+
$result = $builder->buildPhplistEmail(messageId: 123, to: '[email protected]', subject: null, message: 'Body');
98+
$this->assertNull($result);
99+
}
100+
101+
public function testReturnsNullWhenBlacklistedAndHistoryUpdated(): void
102+
{
103+
$this->blacklistRepository->method('isEmailBlacklisted')->willReturn(true);
104+
105+
$subscriber = $this->getMockBuilder(Subscriber::class)
106+
->disableOriginalConstructor()
107+
->onlyMethods(['setBlacklisted'])
108+
->getMock();
109+
$subscriber->expects($this->once())->method('setBlacklisted')->with(true);
110+
111+
$this->subscriberRepository->method('findOneByEmail')->with('[email protected]')->willReturn($subscriber);
112+
$this->subscriberHistoryManager->expects($this->once())->method('addHistory');
113+
114+
$builder = $this->createBuilder();
115+
$result = $builder->buildPhplistEmail(messageId: 55, to: '[email protected]', subject: 'Hi', message: 'Body');
116+
$this->assertNull($result);
117+
}
118+
119+
public function testBuildsEmailWithExpectedHeadersAndBodiesInDevMode(): void
120+
{
121+
$this->blacklistRepository->method('isEmailBlacklisted')->willReturn(false);
122+
123+
// SystemMailConstructor returns both html and text bodies
124+
$this->systemMailConstructor->expects($this->once())
125+
->method('__invoke')
126+
->with('Body', 'Subject')
127+
->willReturn(['<p>HTML</p>', 'TEXT']);
128+
129+
// TemplateImageEmbedder invoked when HTML present
130+
$this->templateImageEmbedder->expects($this->once())
131+
->method('__invoke')
132+
->with(html: '<p>HTML</p>', messageId: 777)
133+
->willReturn('<p>HTML</p>');
134+
135+
$builder = $this->createBuilder(
136+
googleSenderId: 'g-123',
137+
useAmazonSes: false,
138+
usePrecedenceHeader: true,
139+
devVersion: true,
140+
devEmail: '[email protected]'
141+
);
142+
143+
$email = $builder->buildPhplistEmail(
144+
messageId: 777,
145+
146+
subject: 'Subject',
147+
message: 'Body',
148+
skipBlacklistCheck: false,
149+
inBlast: true
150+
);
151+
152+
$this->assertNotNull($email);
153+
154+
// Recipient is redirected to dev email in dev mode
155+
$this->assertCount(1, $email->getTo());
156+
$this->assertInstanceOf(Address::class, $email->getTo()[0]);
157+
$this->assertSame('[email protected]', $email->getTo()[0]->getAddress());
158+
$this->assertSame('', $email->getTo()[0]->getName());
159+
160+
// Basic headers
161+
$headers = $email->getHeaders();
162+
$this->assertTrue($headers->has('X-MessageID'));
163+
$this->assertSame('777', $headers->get('X-MessageID')->getBodyAsString());
164+
165+
$this->assertTrue($headers->has('X-ListMember'));
166+
$this->assertSame('[email protected]', $headers->get('X-ListMember')->getBodyAsString());
167+
168+
$this->assertTrue($headers->has('Feedback-ID'));
169+
$this->assertSame('777:g-123', $headers->get('Feedback-ID')->getBodyAsString());
170+
171+
// Precedence: bulk when not using Amazon SES and enabled
172+
$this->assertTrue($headers->has('Precedence'));
173+
$this->assertSame('bulk', $headers->get('Precedence')->getBodyAsString());
174+
175+
// X-Blast for campaign blasts
176+
$this->assertTrue($headers->has('X-Blast'));
177+
$this->assertSame('1', $headers->get('X-Blast')->getBodyAsString());
178+
179+
// List-Unsubscribe includes the email
180+
$this->assertTrue($headers->has('List-Unsubscribe'));
181+
$this->assertStringContainsString(
182+
'email=dev%40example.com',
183+
$headers->get('List-Unsubscribe')->getBodyAsString()
184+
);
185+
186+
// In dev mode with redirected recipient, no "X-Originally to" header is set per current implementation
187+
$this->assertFalse($headers->has('X-Originally to'));
188+
189+
// Bodies
190+
$this->assertSame('<p>HTML</p>', $email->getHtmlBody());
191+
$this->assertSame('TEXT', $email->getTextBody());
192+
$this->assertSame('Subject', $email->getSubject());
193+
}
194+
}

0 commit comments

Comments
 (0)