vendor/shopware/core/Content/Mail/Service/MailService.php line 103

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Mail\Service;
  3. use Monolog\Logger;
  4. use Psr\Log\LoggerInterface;
  5. use Shopware\Core\Content\MailTemplate\Exception\SalesChannelNotFoundException;
  6. use Shopware\Core\Content\MailTemplate\Service\Event\MailBeforeSentEvent;
  7. use Shopware\Core\Content\MailTemplate\Service\Event\MailBeforeValidateEvent;
  8. use Shopware\Core\Content\MailTemplate\Service\Event\MailErrorEvent;
  9. use Shopware\Core\Content\MailTemplate\Service\Event\MailSentEvent;
  10. use Shopware\Core\Content\Media\MediaCollection;
  11. use Shopware\Core\Content\Media\Pathname\UrlGeneratorInterface;
  12. use Shopware\Core\Framework\Adapter\Twig\StringTemplateRenderer;
  13. use Shopware\Core\Framework\Context;
  14. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Validation\EntityExists;
  18. use Shopware\Core\Framework\Log\Package;
  19. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  20. use Shopware\Core\Framework\Validation\DataValidationDefinition;
  21. use Shopware\Core\Framework\Validation\DataValidator;
  22. use Shopware\Core\System\SalesChannel\SalesChannelDefinition;
  23. use Shopware\Core\System\SalesChannel\SalesChannelEntity;
  24. use Shopware\Core\System\SystemConfig\SystemConfigService;
  25. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  26. use Symfony\Component\Mime\Email;
  27. use Symfony\Component\Validator\Constraints\NotBlank;
  28. #[Package('system-settings')]
  29. class MailService extends AbstractMailService
  30. {
  31.     private DataValidator $dataValidator;
  32.     private StringTemplateRenderer $templateRenderer;
  33.     private AbstractMailFactory $mailFactory;
  34.     private EntityRepositoryInterface $mediaRepository;
  35.     private SalesChannelDefinition $salesChannelDefinition;
  36.     /**
  37.      * @var EntityRepositoryInterface
  38.      */
  39.     private $salesChannelRepository;
  40.     /**
  41.      * @var SystemConfigService
  42.      */
  43.     private $systemConfigService;
  44.     private EventDispatcherInterface $eventDispatcher;
  45.     private UrlGeneratorInterface $urlGenerator;
  46.     private AbstractMailSender $mailSender;
  47.     private LoggerInterface $logger;
  48.     /**
  49.      * @internal
  50.      */
  51.     public function __construct(
  52.         DataValidator $dataValidator,
  53.         StringTemplateRenderer $templateRenderer,
  54.         AbstractMailFactory $mailFactory,
  55.         AbstractMailSender $emailSender,
  56.         EntityRepositoryInterface $mediaRepository,
  57.         SalesChannelDefinition $salesChannelDefinition,
  58.         EntityRepositoryInterface $salesChannelRepository,
  59.         SystemConfigService $systemConfigService,
  60.         EventDispatcherInterface $eventDispatcher,
  61.         UrlGeneratorInterface $urlGenerator,
  62.         LoggerInterface $logger
  63.     ) {
  64.         $this->dataValidator $dataValidator;
  65.         $this->templateRenderer $templateRenderer;
  66.         $this->mailFactory $mailFactory;
  67.         $this->mailSender $emailSender;
  68.         $this->mediaRepository $mediaRepository;
  69.         $this->salesChannelDefinition $salesChannelDefinition;
  70.         $this->salesChannelRepository $salesChannelRepository;
  71.         $this->systemConfigService $systemConfigService;
  72.         $this->eventDispatcher $eventDispatcher;
  73.         $this->urlGenerator $urlGenerator;
  74.         $this->logger $logger;
  75.     }
  76.     public function getDecorated(): AbstractMailService
  77.     {
  78.         throw new DecorationPatternException(self::class);
  79.     }
  80.     /**
  81.      * @param mixed[] $data
  82.      * @param mixed[] $templateData
  83.      */
  84.     public function send(array $dataContext $context, array $templateData = []): ?Email
  85.     {
  86.         $event = new MailBeforeValidateEvent($data$context$templateData);
  87.         $this->eventDispatcher->dispatch($event);
  88.         $data $event->getData();
  89.         $templateData $event->getTemplateData();
  90.         if ($event->isPropagationStopped()) {
  91.             return null;
  92.         }
  93.         $definition $this->getValidationDefinition($context);
  94.         $this->dataValidator->validate($data$definition);
  95.         $recipients $data['recipients'];
  96.         $salesChannelId $data['salesChannelId'];
  97.         $salesChannel null;
  98.         if (($salesChannelId !== null && !isset($templateData['salesChannel'])) || $this->isTestMode($data)) {
  99.             $criteria $this->getSalesChannelDomainCriteria($salesChannelId$context);
  100.             /** @var SalesChannelEntity|null $salesChannel */
  101.             $salesChannel $this->salesChannelRepository->search($criteria$context)->get($salesChannelId);
  102.             if ($salesChannel === null) {
  103.                 throw new SalesChannelNotFoundException($salesChannelId);
  104.             }
  105.             $templateData['salesChannel'] = $salesChannel;
  106.         } elseif ($this->templateDataContainsSalesChannel($templateData)) {
  107.             $salesChannel $templateData['salesChannel'];
  108.         }
  109.         $senderEmail $data['senderMail'] ?? $this->getSender($data$salesChannelId$context);
  110.         if ($senderEmail === null) {
  111.             $event = new MailErrorEvent(
  112.                 $context,
  113.                 Logger::ERROR,
  114.                 null,
  115.                 'senderMail not configured for salesChannel: ' $salesChannelId '. Please check system_config \'core.basicInformation.email\'',
  116.                 null,
  117.                 $templateData
  118.             );
  119.             $this->eventDispatcher->dispatch($event);
  120.             $this->logger->error(
  121.                 'senderMail not configured for salesChannel: ' $salesChannelId '. Please check system_config \'core.basicInformation.email\'',
  122.                 $templateData
  123.             );
  124.         }
  125.         $contents $this->buildContents($data$salesChannel);
  126.         if ($this->isTestMode($data)) {
  127.             $this->templateRenderer->enableTestMode();
  128.             if (!isset($templateData['order']) && !isset($templateData['order']['deepLinkCode']) || $templateData['order']['deepLinkCode'] === '') {
  129.                 $templateData['order']['deepLinkCode'] = 'home';
  130.             }
  131.         }
  132.         $template $data['subject'];
  133.         try {
  134.             $data['subject'] = $this->templateRenderer->render($template$templateData$contextfalse);
  135.             $template $data['senderName'];
  136.             $data['senderName'] = $this->templateRenderer->render($template$templateData$contextfalse);
  137.             foreach ($contents as $index => $template) {
  138.                 $contents[$index] = $this->templateRenderer->render($template$templateData$context$index !== 'text/plain');
  139.             }
  140.         } catch (\Throwable $e) {
  141.             $event = new MailErrorEvent(
  142.                 $context,
  143.                 Logger::ERROR,
  144.                 $e,
  145.                 'Could not render Mail-Template with error message: ' $e->getMessage(),
  146.                 $template,
  147.                 $templateData
  148.             );
  149.             $this->eventDispatcher->dispatch($event);
  150.             $this->logger->error(
  151.                 'Could not render Mail-Template with error message: ' $e->getMessage(),
  152.                 array_merge([
  153.                     'template' => $template,
  154.                     'exception' => (string) $e,
  155.                 ], $templateData)
  156.             );
  157.             return null;
  158.         }
  159.         if (isset($data['testMode']) && (bool) $data['testMode'] === true) {
  160.             $this->templateRenderer->disableTestMode();
  161.         }
  162.         $mediaUrls $this->getMediaUrls($data$context);
  163.         $binAttachments $data['binAttachments'] ?? null;
  164.         $mail $this->mailFactory->create(
  165.             $data['subject'],
  166.             [$senderEmail => $data['senderName']],
  167.             $recipients,
  168.             $contents,
  169.             $mediaUrls,
  170.             $data,
  171.             $binAttachments
  172.         );
  173.         if ($mail->getBody()->toString() === '') {
  174.             $event = new MailErrorEvent(
  175.                 $context,
  176.                 Logger::ERROR,
  177.                 null,
  178.                 'mail body is null',
  179.                 null,
  180.                 $templateData
  181.             );
  182.             $this->eventDispatcher->dispatch($event);
  183.             $this->logger->error(
  184.                 'mail body is null',
  185.                 $templateData
  186.             );
  187.             return null;
  188.         }
  189.         $event = new MailBeforeSentEvent($data$mail$context$templateData['eventName'] ?? null);
  190.         $this->eventDispatcher->dispatch($event);
  191.         if ($event->isPropagationStopped()) {
  192.             return null;
  193.         }
  194.         $this->mailSender->send($mail);
  195.         $event = new MailSentEvent($data['subject'], $recipients$contents$context$templateData['eventName'] ?? null);
  196.         $this->eventDispatcher->dispatch($event);
  197.         return $mail;
  198.     }
  199.     /**
  200.      * @param mixed[] $data
  201.      */
  202.     private function getSender(array $data, ?string $salesChannelIdContext $context): ?string
  203.     {
  204.         $senderEmail $data['senderEmail'] ?? null;
  205.         if ($senderEmail === null || trim($senderEmail) === '') {
  206.             $senderEmail $this->systemConfigService->get('core.basicInformation.email'$salesChannelId);
  207.         }
  208.         if ($senderEmail === null || trim($senderEmail) === '') {
  209.             $senderEmail $this->systemConfigService->get('core.mailerSettings.senderAddress'$salesChannelId);
  210.         }
  211.         if ($senderEmail === null || trim($senderEmail) === '') {
  212.             return null;
  213.         }
  214.         return $senderEmail;
  215.     }
  216.     /**
  217.      * Attaches header and footer to given email bodies
  218.      *
  219.      * @param mixed[] $data
  220.      * e.g. ['contentHtml' => 'foobar', 'contentPlain' => '<h1>foobar</h1>']
  221.      *
  222.      * @return mixed[]
  223.      * e.g. ['text/plain' => '{{foobar}}', 'text/html' => '<h1>{{foobar}}</h1>']
  224.      *
  225.      * @internal
  226.      */
  227.     private function buildContents(array $data, ?SalesChannelEntity $salesChannel): array
  228.     {
  229.         if ($salesChannel && $mailHeaderFooter $salesChannel->getMailHeaderFooter()) {
  230.             $headerPlain $mailHeaderFooter->getTranslation('headerPlain') ?? '';
  231.             $footerPlain $mailHeaderFooter->getTranslation('footerPlain') ?? '';
  232.             $headerHtml $mailHeaderFooter->getTranslation('headerHtml') ?? '';
  233.             $footerHtml $mailHeaderFooter->getTranslation('footerHtml') ?? '';
  234.             return [
  235.                 'text/plain' => sprintf('%s%s%s'$headerPlain$data['contentPlain'], $footerPlain),
  236.                 'text/html' => sprintf('%s%s%s'$headerHtml$data['contentHtml'], $footerHtml),
  237.             ];
  238.         }
  239.         return [
  240.             'text/html' => $data['contentHtml'],
  241.             'text/plain' => $data['contentPlain'],
  242.         ];
  243.     }
  244.     private function getValidationDefinition(Context $context): DataValidationDefinition
  245.     {
  246.         $definition = new DataValidationDefinition('mail_service.send');
  247.         $definition->add('recipients', new NotBlank());
  248.         $definition->add('salesChannelId', new EntityExists(['entity' => $this->salesChannelDefinition->getEntityName(), 'context' => $context]));
  249.         $definition->add('contentHtml', new NotBlank());
  250.         $definition->add('contentPlain', new NotBlank());
  251.         $definition->add('subject', new NotBlank());
  252.         $definition->add('senderName', new NotBlank());
  253.         return $definition;
  254.     }
  255.     /**
  256.      * @param mixed[] $data
  257.      *
  258.      * @return string[]
  259.      */
  260.     private function getMediaUrls(array $dataContext $context): array
  261.     {
  262.         if (!isset($data['mediaIds']) || empty($data['mediaIds'])) {
  263.             return [];
  264.         }
  265.         $criteria = new Criteria($data['mediaIds']);
  266.         $criteria->setTitle('mail-service::resolve-media-ids');
  267.         $media null;
  268.         $mediaRepository $this->mediaRepository;
  269.         $context->scope(Context::SYSTEM_SCOPE, static function (Context $context) use ($criteria$mediaRepository, &$media): void {
  270.             /** @var MediaCollection $media */
  271.             $media $mediaRepository->search($criteria$context)->getElements();
  272.         });
  273.         $urls = [];
  274.         foreach ($media ?? [] as $mediaItem) {
  275.             $urls[] = $this->urlGenerator->getRelativeMediaUrl($mediaItem);
  276.         }
  277.         return $urls;
  278.     }
  279.     private function getSalesChannelDomainCriteria(string $salesChannelIdContext $context): Criteria
  280.     {
  281.         $criteria = new Criteria([$salesChannelId]);
  282.         $criteria->setTitle('mail-service::resolve-sales-channel-domain');
  283.         $criteria->addAssociation('mailHeaderFooter');
  284.         $criteria->getAssociation('domains')
  285.             ->addFilter(
  286.                 new EqualsFilter('languageId'$context->getLanguageId())
  287.             );
  288.         return $criteria;
  289.     }
  290.     /**
  291.      * @param mixed[] $data
  292.      */
  293.     private function isTestMode(array $data = []): bool
  294.     {
  295.         return isset($data['testMode']) && (bool) $data['testMode'] === true;
  296.     }
  297.     /**
  298.      * @param mixed[] $templateData
  299.      */
  300.     private function templateDataContainsSalesChannel(array $templateData): bool
  301.     {
  302.         return isset($templateData['salesChannel']) && $templateData['salesChannel'] instanceof SalesChannelEntity;
  303.     }
  304. }