feat: send mailings over MailJet smtp in dedicated worker process

This commit is contained in:
Björn Fromme
2026-08-12 15:47:04 +02:00
parent a5c98025d0
commit e0a9562fc6
14 changed files with 480 additions and 44 deletions
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Tests\Email;
use App\Email\RemoveBusTransportHeaderListener;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
class RemoveBusTransportHeaderListenerTest extends TestCase
{
public function testTheHeaderIsGoneFromTheDeliveredMail(): void
{
$email = $this->createEmail();
(new RemoveBusTransportHeaderListener())($this->createEvent($email, queued: false));
$this->assertFalse($email->getHeaders()->has('X-Bus-Transport'));
}
/**
* Symfony reads the header off the queued message to pick the messenger transport, so
* removing it here would send the mailing over the shared queue after all.
*/
public function testTheHeaderSurvivesQueueing(): void
{
$email = $this->createEmail();
(new RemoveBusTransportHeaderListener())($this->createEvent($email, queued: true));
$this->assertTrue($email->getHeaders()->has('X-Bus-Transport'));
}
private function createEmail(): Email
{
$email = (new Email())
->from('[email protected]')
->to('[email protected]')
->subject('Betreff')
->text('Nachricht')
;
$email->getHeaders()->addTextHeader('X-Bus-Transport', 'mailing');
return $email;
}
private function createEvent(Email $email, bool $queued): MessageEvent
{
return new MessageEvent(
$email,
new Envelope(new Address('[email protected]'), [new Address('[email protected]')]),
'mailing',
$queued
);
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Tests\MessageHandler;
use App\Message\SendTeamerMailing;
use App\MessageHandler\SendTeamerMailingHandler;
use App\Service\Teamer\TeamerMailingService;
use PHPUnit\Framework\TestCase;
class SendTeamerMailingHandlerTest extends TestCase
{
public function testTheMailingIsHandedToTheServiceUnchanged(): void
{
$mailing = new SendTeamerMailing('Betreff', 'Nachricht', [
['email' => '[email protected]', 'firstName' => 'Anna', 'lastName' => 'Berg', 'fullName' => 'Anna Berg'],
]);
$service = $this->createMock(TeamerMailingService::class);
$service
->expects($this->once())
->method('sendMailing')
->with($this->identicalTo($mailing))
;
(new SendTeamerMailingHandler($service))($mailing);
}
}
@@ -7,9 +7,11 @@ namespace App\Tests\Service\Teamer;
use App\Email\Mailer;
use App\Entity\Teamer;
use App\Entity\User;
use App\Message\SendTeamerMailing;
use App\Model\TeamerFilterDto;
use App\Model\TeamerMailingDto;
use App\Model\TeamerMailingRecipient;
use App\Model\TeamerMailingRecipientsDto;
use App\Repository\TeamerRepository;
use App\Service\Teamer\TeamerMailingService;
use PHPUnit\Framework\MockObject\MockObject;
@@ -32,6 +34,7 @@ class TeamerMailingServiceTest extends TestCase
$this->teamerRepository,
$this->mailer,
$this->createMock(LoggerInterface::class),
'mailing',
);
}
@@ -101,16 +104,8 @@ class TeamerMailingServiceTest extends TestCase
$this->assertSame(5, $recipients->getTotal());
}
public function testSendPersonalisesSubjectAndMessagePerRecipient(): void
public function testSendMailingPersonalisesSubjectAndMessagePerRecipient(): void
{
$this->teamerRepository
->method('getMailingRecipients')
->willReturn([
$this->createRecipient('Anna', 'Berg', '[email protected]'),
$this->createRecipient('Bea', 'Ohm', '[email protected]'),
])
;
$sent = [];
$this->mailer
->expects($this->exactly(2))
@@ -120,19 +115,87 @@ class TeamerMailingServiceTest extends TestCase
})
;
$this->service->sendMailing($this->createMailing(
'Hallo {{vorname}}',
'Servus {{name}}',
$this->createRecipient('Anna', 'Berg', '[email protected]'),
$this->createRecipient('Bea', 'Ohm', '[email protected]')
));
$this->assertSame([
['[email protected]', 'Hallo Anna', 'Servus Anna Berg'],
['[email protected]', 'Hallo Bea', 'Servus Bea Ohm'],
], $sent);
}
/**
* Without both transports a mailing silently leaves over the webhoster's relay on the
* shared queue - it still arrives, so nothing about it looks broken, which is exactly
* why it is pinned here.
*/
public function testSendMailingGoesOutOverTheMailingTransports(): void
{
$this->mailer
->expects($this->once())
->method('createAndSendEmail')
->willReturnCallback(function (array $context, array $options): void {
$this->assertSame('mailing', $options['transport']);
$this->assertSame('mailing', $options['bus_transport']);
})
;
$this->service->sendMailing($this->createMailing(
'Betreff',
'Nachricht',
$this->createRecipient('Anna', 'Berg', '[email protected]')
));
}
public function testSendMailingWithoutRecipientsSendsNothing(): void
{
$this->mailer
->expects($this->never())
->method('createAndSendEmail')
;
$this->service->sendMailing($this->createMailing('Betreff', 'Nachricht'));
}
/**
* A preview has to stay on the default relay and the default queue, or composing a
* mailing would wait on the same worker the mailing itself is queued behind.
*/
public function testPreviewLeavesTheTransportsAlone(): void
{
$this->mailer
->expects($this->once())
->method('createAndSendEmail')
->willReturnCallback(function (array $context, array $options): void {
$this->assertNull($options['transport']);
$this->assertNull($options['bus_transport']);
})
;
$mailingDto = (new TeamerMailingDto())
->setSubject('Hallo {{vorname}}')
->setMessage('Servus {{name}}')
;
$recipients = $this->service->resolveRecipients(new TeamerFilterDto());
$count = $this->service->send($mailingDto, $recipients);
$this->service->sendPreview($mailingDto, (new User())->setEmail('[email protected]'));
}
$this->assertSame(2, $count);
$this->assertSame([
['[email protected]', 'Hallo Anna', 'Servus Anna Berg'],
['[email protected]', 'Hallo Bea', 'Servus Bea Ohm'],
], $sent);
public function testMailingSnapshotSkipsRecipientsWithoutAnAddress(): void
{
$recipients = (new TeamerMailingRecipientsDto())
->addEligible($this->createRecipient('Anna', 'Berg', '[email protected]'))
->addEligible($this->createRecipient('Dana', 'Elf', null))
;
$mailing = SendTeamerMailing::fromRecipients('Betreff', 'Nachricht', $recipients);
$this->assertSame(1, $mailing->getRecipientCount());
$this->assertSame('[email protected]', $mailing->getRecipients()[0]['email']);
$this->assertSame('Anna Berg', $mailing->getRecipients()[0]['fullName']);
}
public function testSendPreviewUsesAdminOwnNameAndMarksTheSubject(): void
@@ -191,6 +254,17 @@ class TeamerMailingServiceTest extends TestCase
$this->assertSame('rita.kern', $values[TeamerMailingService::PLACEHOLDER_FULL_NAME]);
}
private function createMailing(string $subject, string $message, TeamerMailingRecipient ...$recipients): SendTeamerMailing
{
$dto = new TeamerMailingRecipientsDto();
foreach ($recipients as $recipient) {
$dto->addEligible($recipient);
}
return SendTeamerMailing::fromRecipients($subject, $message, $dto);
}
private function createRecipient(
string $firstName,
string $lastName,