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

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