213 lines
7.1 KiB
PHP
213 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Service\Teamer;
|
|
|
|
use App\Email\Mailer;
|
|
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 Psr\Log\LoggerInterface;
|
|
|
|
class TeamerMailingService
|
|
{
|
|
public const PLACEHOLDER_FIRST_NAME = '{{vorname}}';
|
|
public const PLACEHOLDER_LAST_NAME = '{{nachname}}';
|
|
public const PLACEHOLDER_FULL_NAME = '{{name}}';
|
|
|
|
public const PLACEHOLDERS = [
|
|
self::PLACEHOLDER_FIRST_NAME => 'Vorname',
|
|
self::PLACEHOLDER_LAST_NAME => 'Nachname',
|
|
self::PLACEHOLDER_FULL_NAME => 'Vor- und Nachname',
|
|
];
|
|
|
|
private const TEMPLATE = 'email/teamer_mailing.html.twig';
|
|
private const PREVIEW_SUBJECT_PREFIX = '[Vorschau] ';
|
|
|
|
/**
|
|
* The mailer transport a mailing goes out over: Mailjet, rather than the webhoster's
|
|
* relay that carries every other mail, which would risk the hosting account over a few
|
|
* hundred recipients. Configured in mailer.yaml.
|
|
*/
|
|
private const TRANSPORT = 'mailing';
|
|
|
|
public function __construct(
|
|
private readonly TeamerRepository $teamerRepository,
|
|
private readonly Mailer $mailer,
|
|
private readonly LoggerInterface $logger,
|
|
// messenger transport, "mailing" in production and "sync" in dev and test,
|
|
// see the parameter of the same name in services.yaml
|
|
private readonly string $mailingBusTransport,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Split everyone the filter matches into those who will be written to and those who
|
|
* are skipped. Deleted and disabled teamers only show up here at all when the admin
|
|
* asked for them in the filter.
|
|
*/
|
|
public function resolveRecipients(TeamerFilterDto $filterDto): TeamerMailingRecipientsDto
|
|
{
|
|
$recipients = new TeamerMailingRecipientsDto();
|
|
|
|
foreach ($this->teamerRepository->getMailingRecipients($filterDto) as $recipient) {
|
|
if (true === $recipient->isDeleted()) {
|
|
$recipients->addSkippedDeleted();
|
|
|
|
continue;
|
|
}
|
|
|
|
if (true === $recipient->isDisabled()) {
|
|
$recipients->addSkippedDisabled();
|
|
|
|
continue;
|
|
}
|
|
|
|
if (null === $recipient->getEmail()) {
|
|
$recipients->addSkippedNoEmail();
|
|
|
|
continue;
|
|
}
|
|
|
|
$recipients->addEligible($recipient);
|
|
}
|
|
|
|
return $recipients;
|
|
}
|
|
|
|
/**
|
|
* Hand every recipient of a confirmed mailing to the mailer.
|
|
*
|
|
* This runs in the worker, not in the request that confirmed the mailing, so it
|
|
* reports nothing back: each mail becomes a queued message of its own and whether it
|
|
* reaches anyone is decided long after this method returns. A caller that wants to
|
|
* know how many mails were actually delivered has to ask the mail provider.
|
|
*/
|
|
public function sendMailing(SendTeamerMailing $mailing): void
|
|
{
|
|
foreach ($mailing->getRecipients() as $recipient) {
|
|
$values = $this->buildValues(
|
|
$recipient['firstName'],
|
|
$recipient['lastName'],
|
|
$recipient['fullName']
|
|
);
|
|
|
|
$this->sendTo(
|
|
$recipient['email'],
|
|
$this->render($mailing->getSubject(), $values),
|
|
$this->render($mailing->getMessage(), $values),
|
|
self::TRANSPORT,
|
|
$this->mailingBusTransport
|
|
);
|
|
}
|
|
|
|
$this->logger->info('Send teamer mailing', [
|
|
'subject' => $mailing->getSubject(),
|
|
'recipients' => $mailing->getRecipientCount(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Send the very same mail the recipients would get to the composing admin, with their
|
|
* own name filled in for the placeholders.
|
|
*/
|
|
public function sendPreview(TeamerMailingDto $mailingDto, User $admin): void
|
|
{
|
|
$values = $this->placeholderValuesForUser($admin);
|
|
|
|
$this->sendTo(
|
|
$admin->getUserIdentifier(),
|
|
self::PREVIEW_SUBJECT_PREFIX.$this->render($mailingDto->getSubject(), $values),
|
|
$this->render($mailingDto->getMessage(), $values)
|
|
);
|
|
|
|
$this->logger->info('Send teamer mailing preview', [
|
|
'to' => $admin->getEmail(),
|
|
'subject' => $mailingDto->getSubject(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Replace the known placeholders. Anything that merely looks like a placeholder is
|
|
* left alone - a typo should not silently blank out part of the mail.
|
|
*/
|
|
public function render(?string $text, array $values): string
|
|
{
|
|
return strtr((string) $text, $values);
|
|
}
|
|
|
|
public function placeholderValuesFor(TeamerMailingRecipient $recipient): array
|
|
{
|
|
return $this->buildValues(
|
|
$recipient->getFirstName(),
|
|
$recipient->getLastName(),
|
|
$recipient->getFullName()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The preview uses the admin's own teamer record when they have one, so it is exactly
|
|
* what a recipient would see. Admins without one fall back to their user account, and
|
|
* to the local part of their address when even that has no name on it - an empty
|
|
* "Hallo ," in the preview would only be confusing, it says nothing about the real mail.
|
|
*/
|
|
public function placeholderValuesForUser(User $user): array
|
|
{
|
|
if (null !== $teamer = $user->getTeamer()) {
|
|
return $this->buildValues(
|
|
(string) $teamer->getFirstName(),
|
|
(string) $teamer->getLastName(),
|
|
$teamer->getFullName()
|
|
);
|
|
}
|
|
|
|
$firstName = (string) $user->getFirstName();
|
|
$lastName = (string) $user->getLastName();
|
|
$fullName = trim($user->getFullName());
|
|
|
|
if ('' === $firstName && '' === $lastName) {
|
|
$fallback = strstr((string) $user->getEmail(), '@', true) ?: (string) $user->getEmail();
|
|
|
|
$firstName = $fallback;
|
|
$fullName = $fallback;
|
|
}
|
|
|
|
return $this->buildValues($firstName, $lastName, $fullName);
|
|
}
|
|
|
|
private function buildValues(string $firstName, string $lastName, string $fullName): array
|
|
{
|
|
return [
|
|
self::PLACEHOLDER_FIRST_NAME => $firstName,
|
|
self::PLACEHOLDER_LAST_NAME => $lastName,
|
|
self::PLACEHOLDER_FULL_NAME => $fullName,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* The preview leaves the transports unset on purpose: it goes out over the same relay
|
|
* and the same queue as the rest of the application, so it arrives while the admin is
|
|
* still looking at the compose page.
|
|
*/
|
|
private function sendTo(
|
|
string $email,
|
|
string $subject,
|
|
string $message,
|
|
?string $transport = null,
|
|
?string $busTransport = null,
|
|
): void {
|
|
$this->mailer->createAndSendEmail([
|
|
'message' => $message,
|
|
], [
|
|
'to' => $email,
|
|
'subject' => $subject,
|
|
'template' => self::TEMPLATE,
|
|
'transport' => $transport,
|
|
'bus_transport' => $busTransport,
|
|
]);
|
|
}
|
|
}
|