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

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\Content\ContactForm\Event\ContactFormEvent;
  6. use Shopware\Core\Content\Flow\Dispatching\DelayableAction;
  7. use Shopware\Core\Content\Flow\Dispatching\StorableFlow;
  8. use Shopware\Core\Content\Flow\Events\FlowSendMailActionEvent;
  9. use Shopware\Core\Content\Mail\Service\AbstractMailService;
  10. use Shopware\Core\Content\Mail\Service\MailAttachmentsConfig;
  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\Framework\Adapter\Translation\Translator;
  17. use Shopware\Core\Framework\Context;
  18. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  21. use Shopware\Core\Framework\Event\FlowEvent;
  22. use Shopware\Core\Framework\Event\MailAware;
  23. use Shopware\Core\Framework\Event\OrderAware;
  24. use Shopware\Core\Framework\Feature;
  25. use Shopware\Core\Framework\Log\Package;
  26. use Shopware\Core\Framework\Uuid\Uuid;
  27. use Shopware\Core\Framework\Validation\DataBag\DataBag;
  28. use Shopware\Core\System\Locale\LanguageLocaleCodeProvider;
  29. use Symfony\Contracts\EventDispatcher\Event;
  30. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  31. /**
  32. * @deprecated tag:v6.5.0 - reason:remove-subscriber - FlowActions won't be executed over the event system anymore,
  33. * therefore the actions won't implement the EventSubscriberInterface anymore.
  34. */
  35. #[Package('business-ops')]
  36. class SendMailAction extends FlowAction implements DelayableAction
  37. {
  38. public const ACTION_NAME = MailTemplateActions::MAIL_TEMPLATE_MAIL_SEND_ACTION;
  39. public const MAIL_CONFIG_EXTENSION = 'mail-attachments';
  40. private const RECIPIENT_CONFIG_ADMIN = 'admin';
  41. private const RECIPIENT_CONFIG_CUSTOM = 'custom';
  42. private const RECIPIENT_CONFIG_CONTACT_FORM_MAIL = 'contactFormMail';
  43. private EntityRepositoryInterface $mailTemplateRepository;
  44. private LoggerInterface $logger;
  45. private AbstractMailService $emailService;
  46. private EventDispatcherInterface $eventDispatcher;
  47. private EntityRepositoryInterface $mailTemplateTypeRepository;
  48. private Translator $translator;
  49. private Connection $connection;
  50. private LanguageLocaleCodeProvider $languageLocaleProvider;
  51. private bool $updateMailTemplate;
  52. /**
  53. * @internal
  54. */
  55. public function __construct(
  56. AbstractMailService $emailService,
  57. EntityRepositoryInterface $mailTemplateRepository,
  58. LoggerInterface $logger,
  59. EventDispatcherInterface $eventDispatcher,
  60. EntityRepositoryInterface $mailTemplateTypeRepository,
  61. Translator $translator,
  62. Connection $connection,
  63. LanguageLocaleCodeProvider $languageLocaleProvider,
  64. bool $updateMailTemplate
  65. ) {
  66. $this->mailTemplateRepository = $mailTemplateRepository;
  67. $this->logger = $logger;
  68. $this->emailService = $emailService;
  69. $this->eventDispatcher = $eventDispatcher;
  70. $this->mailTemplateTypeRepository = $mailTemplateTypeRepository;
  71. $this->translator = $translator;
  72. $this->connection = $connection;
  73. $this->languageLocaleProvider = $languageLocaleProvider;
  74. $this->updateMailTemplate = $updateMailTemplate;
  75. }
  76. public static function getName(): string
  77. {
  78. return 'action.mail.send';
  79. }
  80. /**
  81. * @deprecated tag:v6.5.0 - reason:remove-subscriber - Will be removed
  82. */
  83. public static function getSubscribedEvents(): array
  84. {
  85. if (Feature::isActive('v6.5.0.0')) {
  86. return [];
  87. }
  88. return [
  89. self::getName() => 'handle',
  90. ];
  91. }
  92. /**
  93. * @return array<string>
  94. */
  95. public function requirements(): array
  96. {
  97. return [MailAware::class];
  98. }
  99. /**
  100. * @deprecated tag:v6.5.0 Will be removed, implement handleFlow instead
  101. *
  102. * @throws MailEventConfigurationException
  103. * @throws SalesChannelNotFoundException
  104. * @throws InconsistentCriteriaIdsException
  105. */
  106. public function handle(Event $event): void
  107. {
  108. Feature::triggerDeprecationOrThrow(
  109. 'v6.5.0.0',
  110. Feature::deprecatedMethodMessage(__CLASS__, __METHOD__, 'v6.5.0.0')
  111. );
  112. if (!$event instanceof FlowEvent) {
  113. return;
  114. }
  115. $mailEvent = $event->getEvent();
  116. $extension = $event->getContext()->getExtension(self::MAIL_CONFIG_EXTENSION);
  117. if (!$extension instanceof MailSendSubscriberConfig) {
  118. $extension = new MailSendSubscriberConfig(false, [], []);
  119. }
  120. if ($extension->skip()) {
  121. return;
  122. }
  123. if (!$mailEvent instanceof MailAware) {
  124. throw new MailEventConfigurationException('Not an instance of MailAware', \get_class($mailEvent));
  125. }
  126. $eventConfig = $event->getConfig();
  127. if (empty($eventConfig['recipient'])) {
  128. throw new MailEventConfigurationException('The recipient value in the flow action configuration is missing.', \get_class($mailEvent));
  129. }
  130. if (!isset($eventConfig['mailTemplateId'])) {
  131. return;
  132. }
  133. $mailTemplate = $this->getMailTemplate($eventConfig['mailTemplateId'], $event->getContext());
  134. if ($mailTemplate === null) {
  135. return;
  136. }
  137. $injectedTranslator = $this->injectTranslator($mailEvent->getContext(), $mailEvent->getSalesChannelId());
  138. $data = new DataBag();
  139. $contactFormData = [];
  140. if ($mailEvent instanceof ContactFormEvent) {
  141. $contactFormData = $mailEvent->getContactFormData();
  142. }
  143. $recipients = $this->getRecipients($eventConfig['recipient'], $mailEvent->getMailStruct()->getRecipients(), $contactFormData);
  144. if (empty($recipients)) {
  145. return;
  146. }
  147. $data->set('recipients', $recipients);
  148. $data->set('senderName', $mailTemplate->getTranslation('senderName'));
  149. $data->set('salesChannelId', $mailEvent->getSalesChannelId());
  150. $data->set('templateId', $mailTemplate->getId());
  151. $data->set('customFields', $mailTemplate->getCustomFields());
  152. $data->set('contentHtml', $mailTemplate->getTranslation('contentHtml'));
  153. $data->set('contentPlain', $mailTemplate->getTranslation('contentPlain'));
  154. $data->set('subject', $mailTemplate->getTranslation('subject'));
  155. $data->set('mediaIds', []);
  156. $data->set('attachmentsConfig', new MailAttachmentsConfig(
  157. $event->getContext(),
  158. $mailTemplate,
  159. $extension,
  160. $eventConfig,
  161. $mailEvent instanceof OrderAware ? $mailEvent->getOrderId() : null,
  162. ));
  163. $this->setReplyTo($data, $eventConfig, $contactFormData);
  164. $this->eventDispatcher->dispatch(new FlowSendMailActionEvent($data, $mailTemplate, $event));
  165. if ($data->has('templateId')) {
  166. $this->updateMailTemplateType(
  167. $event->getContext(),
  168. $event,
  169. $this->getTemplateData($mailEvent),
  170. $mailTemplate
  171. );
  172. }
  173. $templateData = array_merge([
  174. 'eventName' => $mailEvent->getName(),
  175. ], $this->getTemplateData($mailEvent));
  176. $this->send($data, $event->getContext(), $templateData, $extension, $injectedTranslator);
  177. }
  178. /**
  179. * @throws MailEventConfigurationException
  180. * @throws SalesChannelNotFoundException
  181. * @throws InconsistentCriteriaIdsException
  182. */
  183. public function handleFlow(StorableFlow $flow): void
  184. {
  185. $extension = $flow->getContext()->getExtension(self::MAIL_CONFIG_EXTENSION);
  186. if (!$extension instanceof MailSendSubscriberConfig) {
  187. $extension = new MailSendSubscriberConfig(false, [], []);
  188. }
  189. if ($extension->skip()) {
  190. return;
  191. }
  192. if (!$flow->hasStore(MailAware::MAIL_STRUCT) || !$flow->hasStore(MailAware::SALES_CHANNEL_ID)) {
  193. throw new MailEventConfigurationException('Not have data from MailAware', \get_class($flow));
  194. }
  195. $eventConfig = $flow->getConfig();
  196. if (empty($eventConfig['recipient'])) {
  197. throw new MailEventConfigurationException('The recipient value in the flow action configuration is missing.', \get_class($flow));
  198. }
  199. if (!isset($eventConfig['mailTemplateId'])) {
  200. return;
  201. }
  202. $mailTemplate = $this->getMailTemplate($eventConfig['mailTemplateId'], $flow->getContext());
  203. if ($mailTemplate === null) {
  204. return;
  205. }
  206. $injectedTranslator = $this->injectTranslator($flow->getContext(), $flow->getStore(MailAware::SALES_CHANNEL_ID));
  207. $data = new DataBag();
  208. $recipients = $this->getRecipients(
  209. $eventConfig['recipient'],
  210. $flow->getStore(MailAware::MAIL_STRUCT)['recipients'],
  211. $flow->getStore('contactFormData', []),
  212. );
  213. if (empty($recipients)) {
  214. return;
  215. }
  216. $data->set('recipients', $recipients);
  217. $data->set('senderName', $mailTemplate->getTranslation('senderName'));
  218. $data->set('salesChannelId', $flow->getStore(MailAware::SALES_CHANNEL_ID));
  219. $data->set('templateId', $mailTemplate->getId());
  220. $data->set('customFields', $mailTemplate->getCustomFields());
  221. $data->set('contentHtml', $mailTemplate->getTranslation('contentHtml'));
  222. $data->set('contentPlain', $mailTemplate->getTranslation('contentPlain'));
  223. $data->set('subject', $mailTemplate->getTranslation('subject'));
  224. $data->set('mediaIds', []);
  225. $data->set('attachmentsConfig', new MailAttachmentsConfig(
  226. $flow->getContext(),
  227. $mailTemplate,
  228. $extension,
  229. $eventConfig,
  230. $flow->getStore(OrderAware::ORDER_ID),
  231. ));
  232. $this->setReplyTo($data, $eventConfig, $flow->getStore('contactFormData', []));
  233. $this->eventDispatcher->dispatch(new FlowSendMailActionEvent($data, $mailTemplate, $flow));
  234. if ($data->has('templateId')) {
  235. $this->updateMailTemplateType(
  236. $flow->getContext(),
  237. $flow,
  238. $flow->data(),
  239. $mailTemplate
  240. );
  241. }
  242. $templateData = array_merge([
  243. 'eventName' => $flow->getName(),
  244. ], $flow->data());
  245. $this->send($data, $flow->getContext(), $templateData, $extension, $injectedTranslator);
  246. }
  247. /**
  248. * @param array<string, mixed> $templateData
  249. */
  250. private function send(DataBag $data, Context $context, array $templateData, MailSendSubscriberConfig $extension, bool $injectedTranslator): void
  251. {
  252. try {
  253. $this->emailService->send(
  254. $data->all(),
  255. $context,
  256. $templateData
  257. );
  258. } catch (\Exception $e) {
  259. $this->logger->error(
  260. "Could not send mail:\n"
  261. . $e->getMessage() . "\n"
  262. . 'Error Code:' . $e->getCode() . "\n"
  263. . "Template data: \n"
  264. . json_encode($data->all()) . "\n"
  265. );
  266. }
  267. if ($injectedTranslator) {
  268. $this->translator->resetInjection();
  269. }
  270. }
  271. /**
  272. * @param FlowEvent|StorableFlow $event
  273. * @param array<string, mixed> $templateData
  274. */
  275. private function updateMailTemplateType(
  276. Context $context,
  277. $event,
  278. array $templateData,
  279. MailTemplateEntity $mailTemplate
  280. ): void {
  281. if (!$mailTemplate->getMailTemplateTypeId()) {
  282. return;
  283. }
  284. if (!$this->updateMailTemplate) {
  285. return;
  286. }
  287. $mailTemplateTypeTranslation = $this->connection->fetchOne(
  288. 'SELECT 1 FROM mail_template_type_translation WHERE language_id = :languageId AND mail_template_type_id =:mailTemplateTypeId',
  289. [
  290. 'languageId' => Uuid::fromHexToBytes($context->getLanguageId()),
  291. 'mailTemplateTypeId' => Uuid::fromHexToBytes($mailTemplate->getMailTemplateTypeId()),
  292. ]
  293. );
  294. if (!$mailTemplateTypeTranslation) {
  295. // Don't throw errors if this fails // Fix with NEXT-15475
  296. $this->logger->error(
  297. "Could not update mail template type, because translation for this language does not exits:\n"
  298. . 'Flow id: ' . $event->getFlowState()->flowId . "\n"
  299. . 'Sequence id: ' . $event->getFlowState()->getSequenceId()
  300. );
  301. return;
  302. }
  303. $this->mailTemplateTypeRepository->update([[
  304. 'id' => $mailTemplate->getMailTemplateTypeId(),
  305. 'templateData' => $templateData,
  306. ]], $context);
  307. }
  308. private function getMailTemplate(string $id, Context $context): ?MailTemplateEntity
  309. {
  310. $criteria = new Criteria([$id]);
  311. $criteria->setTitle('send-mail::load-mail-template');
  312. $criteria->addAssociation('media.media');
  313. $criteria->setLimit(1);
  314. return $this->mailTemplateRepository
  315. ->search($criteria, $context)
  316. ->first();
  317. }
  318. /**
  319. * @throws MailEventConfigurationException
  320. *
  321. * @return array<string, mixed>
  322. */
  323. private function getTemplateData(MailAware $event): array
  324. {
  325. $data = [];
  326. foreach (array_keys($event::getAvailableData()->toArray()) as $key) {
  327. $getter = 'get' . ucfirst($key);
  328. if (!method_exists($event, $getter)) {
  329. throw new MailEventConfigurationException('Data for ' . $key . ' not available.', \get_class($event));
  330. }
  331. $data[$key] = $event->$getter();
  332. }
  333. return $data;
  334. }
  335. private function injectTranslator(Context $context, ?string $salesChannelId): bool
  336. {
  337. if ($salesChannelId === null) {
  338. return false;
  339. }
  340. if ($this->translator->getSnippetSetId() !== null) {
  341. return false;
  342. }
  343. $this->translator->injectSettings(
  344. $salesChannelId,
  345. $context->getLanguageId(),
  346. $this->languageLocaleProvider->getLocaleForLanguageId($context->getLanguageId()),
  347. $context
  348. );
  349. return true;
  350. }
  351. /**
  352. * @param array<string, mixed> $recipients
  353. * @param array<string, mixed> $mailStructRecipients
  354. * @param array<int|string, mixed> $contactFormData
  355. *
  356. * @return array<int|string, string>
  357. */
  358. private function getRecipients(array $recipients, array $mailStructRecipients, array $contactFormData): array
  359. {
  360. switch ($recipients['type']) {
  361. case self::RECIPIENT_CONFIG_CUSTOM:
  362. return $recipients['data'];
  363. case self::RECIPIENT_CONFIG_ADMIN:
  364. $admins = $this->connection->fetchAllAssociative(
  365. 'SELECT first_name, last_name, email FROM user WHERE admin = true'
  366. );
  367. $emails = [];
  368. foreach ($admins as $admin) {
  369. $emails[$admin['email']] = $admin['first_name'] . ' ' . $admin['last_name'];
  370. }
  371. return $emails;
  372. case self::RECIPIENT_CONFIG_CONTACT_FORM_MAIL:
  373. if (empty($contactFormData)) {
  374. return [];
  375. }
  376. if (!\array_key_exists('email', $contactFormData)) {
  377. return [];
  378. }
  379. return [$contactFormData['email'] => ($contactFormData['firstName'] ?? '') . ' ' . ($contactFormData['lastName'] ?? '')];
  380. default:
  381. return $mailStructRecipients;
  382. }
  383. }
  384. /**
  385. * @param array<string, mixed> $eventConfig
  386. * @param array<int|string, mixed> $contactFormData
  387. */
  388. private function setReplyTo(DataBag $data, array $eventConfig, array $contactFormData): void
  389. {
  390. if (empty($eventConfig['replyTo']) || !\is_string($eventConfig['replyTo'])) {
  391. return;
  392. }
  393. if ($eventConfig['replyTo'] !== self::RECIPIENT_CONFIG_CONTACT_FORM_MAIL) {
  394. $data->set('senderMail', $eventConfig['replyTo']);
  395. return;
  396. }
  397. if (empty($contactFormData['email']) || !\is_string($contactFormData['email'])) {
  398. return;
  399. }
  400. $data->set(
  401. 'senderName',
  402. '{% if contactFormData.firstName is defined %}{{ contactFormData.firstName }}{% endif %} '
  403. . '{% if contactFormData.lastName is defined %}{{ contactFormData.lastName }}{% endif %}'
  404. );
  405. $data->set('senderMail', $contactFormData['email']);
  406. }
  407. }