Passed
Push — master ( 72b2aa...c8dd73 )
by Christian
13:52 queued 11s
created

MailService::getSender()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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