vendor/shopware/core/Content/Flow/Dispatching/Action/SendMailAction.php line 130

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Flow\Dispatching\Action;
  3. use Doctrine\DBAL\Connection;
  4. use Psr\Log\LoggerInterface;
  5. use Shopware\Core\Checkout\Document\DocumentCollection;
  6. use Shopware\Core\Checkout\Document\DocumentService;
  7. use Shopware\Core\Checkout\Document\Service\DocumentGenerator;
  8. use Shopware\Core\Content\ContactForm\Event\ContactFormEvent;
  9. use Shopware\Core\Content\Flow\Events\FlowSendMailActionEvent;
  10. use Shopware\Core\Content\Mail\Service\AbstractMailService;
  11. use Shopware\Core\Content\MailTemplate\Exception\MailEventConfigurationException;
  12. use Shopware\Core\Content\MailTemplate\Exception\SalesChannelNotFoundException;
  13. use Shopware\Core\Content\MailTemplate\MailTemplateActions;
  14. use Shopware\Core\Content\MailTemplate\MailTemplateEntity;
  15. use Shopware\Core\Content\MailTemplate\Subscriber\MailSendSubscriberConfig;
  16. use Shopware\Core\Content\Media\MediaService;
  17. use Shopware\Core\Defaults;
  18. use Shopware\Core\Framework\Adapter\Translation\Translator;
  19. use Shopware\Core\Framework\Context;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  21. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  24. use Shopware\Core\Framework\Event\DelayAware;
  25. use Shopware\Core\Framework\Event\FlowEvent;
  26. use Shopware\Core\Framework\Event\MailAware;
  27. use Shopware\Core\Framework\Event\OrderAware;
  28. use Shopware\Core\Framework\Feature;
  29. use Shopware\Core\Framework\Uuid\Uuid;
  30. use Shopware\Core\Framework\Validation\DataBag\DataBag;
  31. use Shopware\Core\System\Locale\LanguageLocaleCodeProvider;
  32. use Symfony\Contracts\EventDispatcher\Event;
  33. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  34. class SendMailAction extends FlowAction
  35. {
  36.     public const ACTION_NAME MailTemplateActions::MAIL_TEMPLATE_MAIL_SEND_ACTION;
  37.     public const MAIL_CONFIG_EXTENSION 'mail-attachments';
  38.     private const RECIPIENT_CONFIG_ADMIN 'admin';
  39.     private const RECIPIENT_CONFIG_CUSTOM 'custom';
  40.     private const RECIPIENT_CONFIG_CONTACT_FORM_MAIL 'contactFormMail';
  41.     private EntityRepositoryInterface $mailTemplateRepository;
  42.     private MediaService $mediaService;
  43.     private EntityRepositoryInterface $mediaRepository;
  44.     private EntityRepositoryInterface $documentRepository;
  45.     private LoggerInterface $logger;
  46.     private AbstractMailService $emailService;
  47.     private EventDispatcherInterface $eventDispatcher;
  48.     private EntityRepositoryInterface $mailTemplateTypeRepository;
  49.     private Translator $translator;
  50.     private Connection $connection;
  51.     private LanguageLocaleCodeProvider $languageLocaleProvider;
  52.     private bool $updateMailTemplate;
  53.     private DocumentGenerator $documentGenerator;
  54.     private DocumentService $documentService;
  55.     /**
  56.      * @internal
  57.      */
  58.     public function __construct(
  59.         AbstractMailService $emailService,
  60.         EntityRepositoryInterface $mailTemplateRepository,
  61.         MediaService $mediaService,
  62.         EntityRepositoryInterface $mediaRepository,
  63.         EntityRepositoryInterface $documentRepository,
  64.         DocumentService $documentService,
  65.         DocumentGenerator $documentGenerator,
  66.         LoggerInterface $logger,
  67.         EventDispatcherInterface $eventDispatcher,
  68.         EntityRepositoryInterface $mailTemplateTypeRepository,
  69.         Translator $translator,
  70.         Connection $connection,
  71.         LanguageLocaleCodeProvider $languageLocaleProvider,
  72.         bool $updateMailTemplate
  73.     ) {
  74.         $this->mailTemplateRepository $mailTemplateRepository;
  75.         $this->mediaService $mediaService;
  76.         $this->mediaRepository $mediaRepository;
  77.         $this->documentRepository $documentRepository;
  78.         $this->logger $logger;
  79.         $this->emailService $emailService;
  80.         $this->eventDispatcher $eventDispatcher;
  81.         $this->mailTemplateTypeRepository $mailTemplateTypeRepository;
  82.         $this->translator $translator;
  83.         $this->connection $connection;
  84.         $this->languageLocaleProvider $languageLocaleProvider;
  85.         $this->updateMailTemplate $updateMailTemplate;
  86.         $this->documentGenerator $documentGenerator;
  87.         $this->documentService $documentService;
  88.     }
  89.     public static function getName(): string
  90.     {
  91.         return 'action.mail.send';
  92.     }
  93.     public static function getSubscribedEvents(): array
  94.     {
  95.         return [
  96.             self::getName() => 'handle',
  97.         ];
  98.     }
  99.     public function requirements(): array
  100.     {
  101.         return [MailAware::class, DelayAware::class];
  102.     }
  103.     /**
  104.      * @throws MailEventConfigurationException
  105.      * @throws SalesChannelNotFoundException
  106.      * @throws InconsistentCriteriaIdsException
  107.      */
  108.     public function handle(Event $event): void
  109.     {
  110.         if (!$event instanceof FlowEvent) {
  111.             return;
  112.         }
  113.         $mailEvent $event->getEvent();
  114.         $extension $event->getContext()->getExtension(self::MAIL_CONFIG_EXTENSION);
  115.         if (!$extension instanceof MailSendSubscriberConfig) {
  116.             $extension = new MailSendSubscriberConfig(false, [], []);
  117.         }
  118.         if ($extension->skip()) {
  119.             return;
  120.         }
  121.         if (!$mailEvent instanceof MailAware) {
  122.             throw new MailEventConfigurationException('Not an instance of MailAware', \get_class($mailEvent));
  123.         }
  124.         $eventConfig $event->getConfig();
  125.         if (empty($eventConfig['recipient'])) {
  126.             throw new MailEventConfigurationException('The recipient value in the flow action configuration is missing.', \get_class($mailEvent));
  127.         }
  128.         if (!isset($eventConfig['mailTemplateId'])) {
  129.             return;
  130.         }
  131.         $mailTemplate $this->getMailTemplate($eventConfig['mailTemplateId'], $event->getContext());
  132.         if ($mailTemplate === null) {
  133.             return;
  134.         }
  135.         $injectedTranslator $this->injectTranslator($mailEvent);
  136.         $data = new DataBag();
  137.         $recipients $this->getRecipients($eventConfig['recipient'], $mailEvent);
  138.         if (empty($recipients)) {
  139.             return;
  140.         }
  141.         $data->set('recipients'$recipients);
  142.         $data->set('senderName'$mailTemplate->getTranslation('senderName'));
  143.         $data->set('salesChannelId'$mailEvent->getSalesChannelId());
  144.         $data->set('templateId'$mailTemplate->getId());
  145.         $data->set('customFields'$mailTemplate->getCustomFields());
  146.         $data->set('contentHtml'$mailTemplate->getTranslation('contentHtml'));
  147.         $data->set('contentPlain'$mailTemplate->getTranslation('contentPlain'));
  148.         $data->set('subject'$mailTemplate->getTranslation('subject'));
  149.         $data->set('mediaIds', []);
  150.         $attachments array_unique($this->buildAttachments($mailEvent$mailTemplate$extension$eventConfig), \SORT_REGULAR);
  151.         if (!empty($attachments)) {
  152.             $data->set('binAttachments'$attachments);
  153.         }
  154.         $this->eventDispatcher->dispatch(new FlowSendMailActionEvent($data$mailTemplate$event));
  155.         if ($data->has('templateId')) {
  156.             $this->updateMailTemplateType($event$mailEvent$mailTemplate);
  157.         }
  158.         try {
  159.             $this->emailService->send(
  160.                 $data->all(),
  161.                 $event->getContext(),
  162.                 $this->getTemplateData($mailEvent)
  163.             );
  164.             $documentAttachments array_filter($attachments, function (array $attachment) use ($extension) {
  165.                 return \array_key_exists('id'$attachment) && \in_array($attachment['id'], $extension->getDocumentIds(), true);
  166.             });
  167.             $documentAttachments array_column($documentAttachments'id');
  168.             if (!empty($documentAttachments)) {
  169.                 $this->connection->executeStatement(
  170.                     'UPDATE `document` SET `updated_at` = :now, `sent` = 1 WHERE `id` IN (:ids)',
  171.                     ['ids' => Uuid::fromHexToBytesList($documentAttachments), 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT)],
  172.                     ['ids' => Connection::PARAM_STR_ARRAY]
  173.                 );
  174.             }
  175.         } catch (\Exception $e) {
  176.             $this->logger->error(
  177.                 "Could not send mail:\n"
  178.                 $e->getMessage() . "\n"
  179.                 'Error Code:' $e->getCode() . "\n"
  180.                 "Template data: \n"
  181.                 json_encode($data->all()) . "\n"
  182.             );
  183.         }
  184.         if ($injectedTranslator) {
  185.             $this->translator->resetInjection();
  186.         }
  187.     }
  188.     private function updateMailTemplateType(FlowEvent $eventMailAware $mailAwareMailTemplateEntity $mailTemplate): void
  189.     {
  190.         if (!$mailTemplate->getMailTemplateTypeId()) {
  191.             return;
  192.         }
  193.         if (!$this->updateMailTemplate) {
  194.             return;
  195.         }
  196.         $mailTemplateTypeTranslation $this->connection->fetchOne(
  197.             'SELECT 1 FROM mail_template_type_translation WHERE language_id = :languageId AND mail_template_type_id =:mailTemplateTypeId',
  198.             [
  199.                 'languageId' => Uuid::fromHexToBytes($event->getContext()->getLanguageId()),
  200.                 'mailTemplateTypeId' => Uuid::fromHexToBytes($mailTemplate->getMailTemplateTypeId()),
  201.             ]
  202.         );
  203.         if (!$mailTemplateTypeTranslation) {
  204.             // Don't throw errors if this fails // Fix with NEXT-15475
  205.             $this->logger->error(
  206.                 "Could not update mail template type, because translation for this language does not exits:\n"
  207.                 'Flow id: ' $event->getFlowState()->flowId "\n"
  208.                 'Sequence id: ' $event->getFlowState()->getSequenceId()
  209.             );
  210.             return;
  211.         }
  212.         $this->mailTemplateTypeRepository->update([[
  213.             'id' => $mailTemplate->getMailTemplateTypeId(),
  214.             'templateData' => $this->getTemplateData($mailAware),
  215.         ]], $mailAware->getContext());
  216.     }
  217.     private function getMailTemplate(string $idContext $context): ?MailTemplateEntity
  218.     {
  219.         $criteria = new Criteria([$id]);
  220.         $criteria->setTitle('send-mail::load-mail-template');
  221.         $criteria->addAssociation('media.media');
  222.         $criteria->setLimit(1);
  223.         return $this->mailTemplateRepository
  224.             ->search($criteria$context)
  225.             ->first();
  226.     }
  227.     /**
  228.      * @throws MailEventConfigurationException
  229.      */
  230.     private function getTemplateData(MailAware $event): array
  231.     {
  232.         $data = [];
  233.         foreach (array_keys($event::getAvailableData()->toArray()) as $key) {
  234.             $getter 'get' ucfirst($key);
  235.             if (!method_exists($event$getter)) {
  236.                 throw new MailEventConfigurationException('Data for ' $key ' not available.', \get_class($event));
  237.             }
  238.             $data[$key] = $event->$getter();
  239.         }
  240.         return $data;
  241.     }
  242.     private function buildAttachments(MailAware $mailEventMailTemplateEntity $mailTemplateMailSendSubscriberConfig $extensions, array $eventConfig): array
  243.     {
  244.         $attachments = [];
  245.         if ($mailTemplate->getMedia() !== null) {
  246.             foreach ($mailTemplate->getMedia() as $mailTemplateMedia) {
  247.                 if ($mailTemplateMedia->getMedia() === null) {
  248.                     continue;
  249.                 }
  250.                 if ($mailTemplateMedia->getLanguageId() !== null && $mailTemplateMedia->getLanguageId() !== $mailEvent->getContext()->getLanguageId()) {
  251.                     continue;
  252.                 }
  253.                 $attachments[] = $this->mediaService->getAttachment(
  254.                     $mailTemplateMedia->getMedia(),
  255.                     $mailEvent->getContext()
  256.                 );
  257.             }
  258.         }
  259.         if (!empty($extensions->getMediaIds())) {
  260.             $criteria = new Criteria($extensions->getMediaIds());
  261.             $criteria->setTitle('send-mail::load-media');
  262.             $entities $this->mediaRepository->search($criteria$mailEvent->getContext());
  263.             foreach ($entities as $media) {
  264.                 $attachments[] = $this->mediaService->getAttachment($media$mailEvent->getContext());
  265.             }
  266.         }
  267.         $documentIds $extensions->getDocumentIds();
  268.         if (!empty($eventConfig['documentTypeIds']) && \is_array($eventConfig['documentTypeIds']) && $mailEvent instanceof OrderAware) {
  269.             $latestDocuments $this->getLatestDocumentsOfTypes($mailEvent->getOrderId(), $eventConfig['documentTypeIds']);
  270.             $documentIds array_unique(array_merge($documentIds$latestDocuments));
  271.         }
  272.         if (!empty($documentIds)) {
  273.             $extensions->setDocumentIds($documentIds);
  274.             if (Feature::isActive('v6.5.0.0')) {
  275.                 $attachments $this->mappingAttachments($documentIds$attachments$mailEvent->getContext());
  276.             } else {
  277.                 $attachments $this->buildOrderAttachments($documentIds$attachments$mailEvent->getContext());
  278.             }
  279.         }
  280.         return $attachments;
  281.     }
  282.     private function injectTranslator(MailAware $event): bool
  283.     {
  284.         if ($event->getSalesChannelId() === null) {
  285.             return false;
  286.         }
  287.         if ($this->translator->getSnippetSetId() !== null) {
  288.             return false;
  289.         }
  290.         $this->translator->injectSettings(
  291.             $event->getSalesChannelId(),
  292.             $event->getContext()->getLanguageId(),
  293.             $this->languageLocaleProvider->getLocaleForLanguageId($event->getContext()->getLanguageId()),
  294.             $event->getContext()
  295.         );
  296.         return true;
  297.     }
  298.     private function getRecipients(array $recipientsMailAware $mailEvent): array
  299.     {
  300.         switch ($recipients['type']) {
  301.             case self::RECIPIENT_CONFIG_CUSTOM:
  302.                 return $recipients['data'];
  303.             case self::RECIPIENT_CONFIG_ADMIN:
  304.                 $admins $this->connection->fetchAllAssociative(
  305.                     'SELECT first_name, last_name, email FROM user WHERE admin = true'
  306.                 );
  307.                 $emails = [];
  308.                 foreach ($admins as $admin) {
  309.                     $emails[$admin['email']] = $admin['first_name'] . ' ' $admin['last_name'];
  310.                 }
  311.                 return $emails;
  312.             case self::RECIPIENT_CONFIG_CONTACT_FORM_MAIL:
  313.                 if (!$mailEvent instanceof ContactFormEvent) {
  314.                     return [];
  315.                 }
  316.                 $data $mailEvent->getContactFormData();
  317.                 if (!\array_key_exists('email'$data)) {
  318.                     return [];
  319.                 }
  320.                 return [$data['email'] => ($data['firstName'] ?? '') . ' ' . ($data['lastName'] ?? '')];
  321.             default:
  322.                 return $mailEvent->getMailStruct()->getRecipients();
  323.         }
  324.     }
  325.     /**
  326.      * @param array<string> $documentIds
  327.      */
  328.     private function buildOrderAttachments(array $documentIds, array $attachmentsContext $context): array
  329.     {
  330.         $criteria = new Criteria($documentIds);
  331.         $criteria->setTitle('send-mail::load-attachments');
  332.         $criteria->addAssociation('documentMediaFile');
  333.         $criteria->addAssociation('documentType');
  334.         /** @var DocumentCollection $documents */
  335.         $documents $this->documentRepository->search($criteria$context)->getEntities();
  336.         return $this->mappingAttachmentsInfo($documents$attachments$context);
  337.     }
  338.     /**
  339.      * @param array<string> $documentTypeIds
  340.      */
  341.     private function getLatestDocumentsOfTypes(string $orderId, array $documentTypeIds): array
  342.     {
  343.         $documents $this->connection->fetchAllAssociative(
  344.             'SELECT
  345.                 LOWER(hex(`document`.`document_type_id`)) as doc_type,
  346.                 LOWER(hex(`document`.`id`)) as doc_id,
  347.                 `document`.`created_at` as newest_date
  348.             FROM
  349.                 `document`
  350.             WHERE
  351.                 HEX(`document`.`order_id`) = :orderId
  352.                 AND HEX(`document`.`document_type_id`) IN (:documentTypeIds)
  353.             ORDER BY `document`.`created_at` DESC',
  354.             [
  355.                 'orderId' => $orderId,
  356.                 'documentTypeIds' => $documentTypeIds,
  357.             ],
  358.             [
  359.                 'documentTypeIds' => Connection::PARAM_STR_ARRAY,
  360.             ]
  361.         );
  362.         $documentsGroupByType FetchModeHelper::group($documents);
  363.         $documentIds = [];
  364.         foreach ($documentsGroupByType as $document) {
  365.             $documentIds[] = array_shift($document)['doc_id'];
  366.         }
  367.         return $documentIds;
  368.     }
  369.     private function mappingAttachmentsInfo(DocumentCollection $documents, array $attachmentsContext $context): array
  370.     {
  371.         foreach ($documents as $document) {
  372.             $documentId $document->getId();
  373.             $document $this->documentService->getDocument($document$context);
  374.             $attachments[] = [
  375.                 'id' => $documentId,
  376.                 'content' => $document->getFileBlob(),
  377.                 'fileName' => $document->getFilename(),
  378.                 'mimeType' => $document->getContentType(),
  379.             ];
  380.         }
  381.         return $attachments;
  382.     }
  383.     /**
  384.      * @param array<string> $documentIds
  385.      */
  386.     private function mappingAttachments(array $documentIds, array $attachmentsContext $context): array
  387.     {
  388.         foreach ($documentIds as $documentId) {
  389.             $document $this->documentGenerator->readDocument($documentId$context);
  390.             if ($document === null) {
  391.                 continue;
  392.             }
  393.             $attachments[] = [
  394.                 'id' => $documentId,
  395.                 'content' => $document->getContent(),
  396.                 'fileName' => $document->getName(),
  397.                 'mimeType' => $document->getContentType(),
  398.             ];
  399.         }
  400.         return $attachments;
  401.     }
  402. }