Completed
Push — master ( c37102...86711a )
by Dominik
18:10
created

AzineTwigSwiftMailer::sendMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 4
crap 1
1
<?php
2
3
namespace Azine\EmailBundle\Services;
4
5
use Azine\EmailBundle\DependencyInjection\AzineEmailExtension;
6
use Azine\EmailBundle\Entity\SentEmail;
7
use Doctrine\Common\Persistence\ManagerRegistry;
8
use FOS\UserBundle\Mailer\TwigSwiftMailer;
9
use Symfony\Bundle\FrameworkBundle\Translation\Translator;
10
use Symfony\Component\HttpFoundation\File\Exception\FileException;
11
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
12
use Symfony\Component\Routing\RequestContext;
13
14
/**
15
 * This Service is used to send html-emails with embedded images.
16
 *
17
 * @author Dominik Businger
18
 */
19
class AzineTwigSwiftMailer extends TwigSwiftMailer implements TemplateTwigSwiftMailerInterface
20
{
21
    /**
22
     * @var Translator
23
     */
24
    protected $translator;
25
26
    /**
27
     * @var TemplateProviderInterface
28
     */
29
    protected $templateProvider;
30
31
    /**
32
     * @var ManagerRegistry
33
     */
34
    protected $managerRegistry;
35
36
    /**
37
     * @var RequestContext
38
     */
39
    protected $routerContext;
40
41
    /**
42
     * @var string email to use for "no-reply"
43
     */
44
    protected $noReplyEmail;
45
46
    /**
47
     * @var string name to use for "no-reply"
48
     */
49
    protected $noReplyName;
50
51
    /**
52
     * The Swift_Mailer to be used for sending emails immediately.
53
     *
54
     * @var \Swift_Mailer
55
     */
56
    private $immediateMailer;
57
58
    /**
59
     * @var EmailOpenTrackingCodeBuilderInterface
60
     */
61
    private $emailOpenTrackingCodeBuilder;
62
63
    /**
64
     * @var AzineEmailTwigExtension
65
     */
66
    private $emailTwigExtension;
67
68
    private $encodedItemIdPattern;
69
    private $templateCache = array();
70
    private $imageCache = array();
71
72
    /**
73
     * @param \Swift_Mailer                         $mailer
74
     * @param UrlGeneratorInterface                 $router
75
     * @param \Twig_Environment                     $twig
76
     * @param Translator                            $translator
77
     * @param TemplateProviderInterface             $templateProvider
78
     * @param ManagerRegistry                       $managerRegistry
79
     * @param EmailOpenTrackingCodeBuilderInterface $emailOpenTrackingCodeBuilder
80
     * @param AzineEmailTwigExtension               $emailTwigExtension
81
     * @param array                                 $parameters
82
     * @param \Swift_Mailer                         $immediateMailer
83
     */
84 7
    public function __construct(\Swift_Mailer $mailer,
85
                                    UrlGeneratorInterface $router,
86
                                    \Twig_Environment $twig,
87
                                    Translator $translator,
88
                                    TemplateProviderInterface $templateProvider,
89
                                    ManagerRegistry $managerRegistry,
90
                                    EmailOpenTrackingCodeBuilderInterface $emailOpenTrackingCodeBuilder,
91
                                    AzineEmailTwigExtension $emailTwigExtension,
92
                                    array $parameters,
93
                                    \Swift_Mailer $immediateMailer = null)
94
    {
95 7
        parent::__construct($mailer, $router, $twig, $parameters);
96 7
        $this->immediateMailer = $immediateMailer;
97 7
        $this->translator = $translator;
98 7
        $this->templateProvider = $templateProvider;
99 7
        $this->managerRegistry = $managerRegistry;
100 7
        $this->noReplyEmail = $parameters[AzineEmailExtension::NO_REPLY][AzineEmailExtension::NO_REPLY_EMAIL_ADDRESS];
101 7
        $this->noReplyName = $parameters[AzineEmailExtension::NO_REPLY][AzineEmailExtension::NO_REPLY_EMAIL_NAME];
102 7
        $this->emailOpenTrackingCodeBuilder = $emailOpenTrackingCodeBuilder;
103 7
        $this->routerContext = $router->getContext();
104 7
        $this->encodedItemIdPattern = '/^cid:.*@/';
105 7
        $this->emailTwigExtension = $emailTwigExtension;
106 7
    }
107
108
    /**
109
     * (non-PHPdoc).
110
     *
111
     * @see Azine\EmailBundle\Services.TemplateTwigSwiftMailerInterface::sendEmail()
112
     *
113
     * @param array        $failedRecipients
114
     * @param string       $subject
115
     * @param string       $from
116
     * @param string       $fromName
117
     * @param array|string $to
118
     * @param string       $toName
119
     * @param array|string $cc
120
     * @param string       $ccName
121
     * @param array|string $bcc
122
     * @param string       $bccName
123
     * @param $replyTo
124
     * @param $replyToName
125
     * @param array $params
126
     * @param $template
127
     * @param array          $attachments
128
     * @param null           $emailLocale
129
     * @param \Swift_Message $message
130
     *
131
     * @return int
132
     */
133 7
    public function sendEmail(&$failedRecipients, $subject, $from, $fromName, $to, $toName, $cc, $ccName, $bcc, $bccName, $replyTo, $replyToName, array $params, $template, $attachments = array(), $emailLocale = null, \Swift_Message &$message = null)
134
    {
135
        // create the message
136 7
        if (null === $message) {
137 7
            $message = \Swift_Message::newInstance();
138
        }
139
140 7
        $message->setSubject($subject);
141
142
        // set the from-Name & -Email to the default ones if not given
143 7
        if (null === $from) {
144 2
            $from = $this->noReplyEmail;
145 2
            if (null === $fromName) {
146 2
                $fromName = $this->noReplyName;
147
            }
148
        }
149
150
        // add the from-email for the footer-text
151 7
        if (!array_key_exists('fromEmail', $params)) {
152 7
            $params['sendMailAccountName'] = $this->noReplyName;
153 7
            $params['sendMailAccountAddress'] = $this->noReplyEmail;
154
        }
155
156
        // get the baseTemplate. => templateId without the ending.
157 7
        $templateBaseId = substr($template, 0, strrpos($template, '.', -6));
158
159
        // check if this email should be stored for web-view
160 7
        if ($this->templateProvider->saveWebViewFor($templateBaseId)) {
161
            // keep a copy of the vars for the web-view
162 4
            $webViewParams = $params;
163
164
            // add the web-view token
165 4
            $params[$this->templateProvider->getWebViewTokenId()] = SentEmail::getNewToken();
166
        } else {
167 3
            $webViewParams = array();
168
        }
169
170
        // recursively add all template-variables for the wrapper-templates and contentItems
171 7
        $params = $this->templateProvider->addTemplateVariablesFor($templateBaseId, $params);
172
173
        // recursively attach all messages in the array
174 7
        $this->embedImages($message, $params);
175
176
        // change the locale for the email-recipients
177 7
        if (null !== $emailLocale && strlen($emailLocale) > 0) {
178 6
            $currentUserLocale = $this->translator->getLocale();
179
180
            // change the router-context locale
181 6
            $this->routerContext->setParameter('_locale', $emailLocale);
182
183
            // change the translator locale
184 6
            $this->translator->setLocale($emailLocale);
185
        } else {
186 1
            $emailLocale = $this->translator->getLocale();
187
        }
188
189
        // recursively add snippets for the wrapper-templates and contentItems
190 7
        $params = $this->templateProvider->addTemplateSnippetsWithImagesFor($templateBaseId, $params, $emailLocale);
191
192
        // add the emailLocale (used for web-view)
193 7
        $params['emailLocale'] = $emailLocale;
194
195
        // render the email parts
196 7
        $twigTemplate = $this->loadTemplate($template);
197 7
        $textBody = $twigTemplate->renderBlock('body_text', $params);
198 7
        $message->addPart($textBody, 'text/plain');
199
200 7
        $htmlBody = $twigTemplate->renderBlock('body_html', $params);
201
202 7
        $campaignParams = $this->templateProvider->getCampaignParamsFor($templateBaseId, $params);
203
204 7
        if (sizeof($campaignParams) > 0) {
205 5
            $htmlBody = $this->emailTwigExtension->addCampaignParamsToAllUrls($htmlBody, $campaignParams);
206
        }
207
208
        // if email-tracking is enabled
209 7
        if ($this->emailOpenTrackingCodeBuilder) {
210
            // add an image at the end of the html tag with the tracking-params to track email-opens
211 7
            $imgTrackingCode = $this->emailOpenTrackingCodeBuilder->getTrackingImgCode($templateBaseId, $campaignParams, $params, $message->getId(), $to, $cc, $bcc);
212 7
            if ($imgTrackingCode && strlen($imgTrackingCode) > 0) {
213 5
                $htmlCloseTagPosition = strpos($htmlBody, '</body>');
214 5
                $htmlBody = substr_replace($htmlBody, $imgTrackingCode, $htmlCloseTagPosition, 0);
215
            }
216
        }
217
218 7
        $message->setBody($htmlBody, 'text/html');
219
220
        // remove unused/unreferenced embeded items from the message
221 7
        $message = $this->removeUnreferecedEmbededItemsFromMessage($message, $params, $htmlBody);
222
223
        // change the locale back to the users locale
224 7
        if (isset($currentUserLocale) && null !== $currentUserLocale) {
225 6
            $this->routerContext->setParameter('_locale', $currentUserLocale);
226 6
            $this->translator->setLocale($currentUserLocale);
227
        }
228
229
        // add attachments
230 7
        foreach ($attachments as $fileName => $file) {
231
            // add attachment from existing file
232 2
            if (is_string($file)) {
233
                // check that the file really exists!
234 2
                if (file_exists($file)) {
235 1
                    $attachment = \Swift_Attachment::fromPath($file);
236 1
                    if (strlen($fileName) >= 5) {
237 1
                        $attachment->setFilename($fileName);
238
                    }
239
                } else {
240 2
                    throw new FileException('File not found: '.$file);
241
                }
242
243
                // add attachment from generated data
244
            } else {
245 1
                $attachment = \Swift_Attachment::newInstance($file, $fileName);
246
            }
247
248 1
            $message->attach($attachment);
249
        }
250
251
        // set the addresses
252 6
        if ($from) {
253 6
            $message->setFrom($from, $fromName);
254
        }
255 6
        if ($replyTo) {
256 2
            $message->setReplyTo($replyTo, $replyToName);
257 4
        } elseif ($from) {
258 4
            $message->setReplyTo($from, $fromName);
259
        }
260 6
        if ($to) {
261 6
            $message->setTo($to, $toName);
262
        }
263 6
        if ($cc) {
264 2
            $message->setCc($cc, $ccName);
265
        }
266 6
        if ($bcc) {
267 2
            $message->setBcc($bcc, $bccName);
268
        }
269
270
        // add custom headers
271 6
        $this->templateProvider->addCustomHeaders($templateBaseId, $message, $params);
272
273
        // send the message
274 6
        $mailer = $this->getMailer($params);
275 6
        $messagesSent = $mailer->send($message, $failedRecipients);
276
277
        // if the message was successfully sent,
278
        // and it should be made available in web-view
279 6
        if ($messagesSent && array_key_exists($this->templateProvider->getWebViewTokenId(), $params)) {
280
            // store the email
281 2
            $sentEmail = new SentEmail();
282 2
            $sentEmail->setToken($params[$this->templateProvider->getWebViewTokenId()]);
283 2
            $sentEmail->setTemplate($templateBaseId);
284 2
            $sentEmail->setSent(new \DateTime());
285
286
            // recursively add all template-variables for the wrapper-templates and contentItems
287 2
            $webViewParams = $this->templateProvider->addTemplateVariablesFor($template, $webViewParams);
288
289
            // replace absolute image-paths with relative ones.
290 2
            $webViewParams = $this->templateProvider->makeImagePathsWebRelative($webViewParams, $emailLocale);
291
292
            // recursively add snippets for the wrapper-templates and contentItems
293 2
            $webViewParams = $this->templateProvider->addTemplateSnippetsWithImagesFor($template, $webViewParams, $emailLocale, true);
294
295 2
            $sentEmail->setVariables($webViewParams);
296
297
            // save only successfull recipients
298 2
            if (!is_array($to)) {
299 2
                $to = array($to);
300
            }
301 2
            $successfulRecipients = array_diff($to, $failedRecipients);
302 2
            $sentEmail->setRecipients($successfulRecipients);
303
304
            // write to db
305 2
            $em = $this->managerRegistry->getManager();
306 2
            $em->persist($sentEmail);
307 2
            $em->flush($sentEmail);
308 2
            $em->clear();
309 2
            gc_collect_cycles();
310
        }
311
312 6
        return $messagesSent;
313
    }
314
315
    /**
316
     * Remove all Embeded Attachments that are not referenced in the html-body from the message
317
     * to avoid using unneccary bandwidth.
318
     *
319
     * @param \Swift_Message $message
320
     * @param array          $params   the parameters used to render the html
321
     * @param string         $htmlBody
322
     *
323
     * @return \Swift_Message
324
     */
325 7
    private function removeUnreferecedEmbededItemsFromMessage(\Swift_Message $message, $params, $htmlBody)
326
    {
327 7
        foreach ($params as $key => $value) {
328
            // remove unreferenced attachments from contentItems too.
329 7
            if ('contentItems' === $key) {
330 2
                foreach ($value as $contentItemParams) {
331 2
                    $message = $this->removeUnreferecedEmbededItemsFromMessage($message, $contentItemParams, $htmlBody);
332
                }
333
            } else {
334
                // check if the embeded items are referenced in the templates
335 7
                $isEmbededItem = is_string($value) && 1 == preg_match($this->encodedItemIdPattern, $value);
336
337 7
                if ($isEmbededItem && false === stripos($htmlBody, $value)) {
338
                    // remove unreferenced items
339 7
                    $children = array();
340
341 7
                    foreach ($message->getChildren() as $attachment) {
342 7
                        if ('cid:'.$attachment->getId() != $value) {
343 7
                            $children[] = $attachment;
344
                        }
345
                    }
346
347 7
                    $message->setChildren($children);
348
                }
349
            }
350
        }
351
352 7
        return $message;
353
    }
354
355
    /**
356
     * Get the template from the cache if it was loaded already.
357
     *
358
     * @param string $template
359
     *
360
     * @return \Twig_Template
361
     */
362 7
    private function loadTemplate($template)
363
    {
364 7
        if (!array_key_exists($template, $this->templateCache)) {
365 7
            $this->templateCache[$template] = $this->twig->loadTemplate($template);
366
        }
367
368 7
        return $this->templateCache[$template];
369
    }
370
371
    /**
372
     * Recursively embed all images in the array into the message.
373
     *
374
     * @param \Swift_Message $message
375
     * @param array          $params
376
     *
377
     * @return array $params
378
     */
379 7
    private function embedImages(&$message, &$params)
380
    {
381
        // loop through the array
382 7
        foreach ($params as $key => $value) {
383
            // if the current value is an array
384 7
            if (is_array($value)) {
385
                // search for more images deeper in the arrays
386 2
                $value = $this->embedImages($message, $value);
387 2
                $params[$key] = $value;
388
389
            // if the current value is an existing file from the image-folder, embed it
390 7
            } elseif (is_string($value)) {
391 7
                if (is_file($value)) {
392
                    // check if the file is from an allowed folder
393 7
                    if (false !== $this->templateProvider->isFileAllowed($value)) {
394 7
                        $encodedImage = $this->cachedEmbedImage($value);
395 7
                        if (null !== $encodedImage) {
396 7
                            $id = $message->embed($encodedImage);
397 7
                            $params[$key] = $id;
398
                        }
399
                    }
400
401
                    // the $filePath isn't a regular file
402
                } else {
403
                    // add a null-value to the cache for this path, so we don't try again.
404 7
                    $this->imageCache[$value] = null;
405
                }
406
407
                //if the current value is a generated image
408 7
            } elseif (is_resource($value) && 0 == stripos(get_resource_type($value), 'gd')) {
409
                // get the image-data as string
410 1
                ob_start();
411 1
                imagepng($value);
412 1
                $imageData = ob_get_clean();
413
414
                // encode the image
415 1
                $encodedImage = \Swift_Image::newInstance($imageData, 'generatedImage'.md5($imageData));
416 1
                $id = $message->embed($encodedImage);
417 7
                $params[$key] = $id;
418
            }
419
            // don't do anything
420
        }
421
422
        // remove duplicate-attachments
423 7
        $message->setChildren(array_unique($message->getChildren()));
424
425 7
        return $params;
426
    }
427
428
    /**
429
     * Get the Swift_Image for the file.
430
     *
431
     * @param string $filePath
432
     *
433
     * @return \Swift_Image|null
434
     */
435 7
    private function cachedEmbedImage($filePath)
436
    {
437 7
        $filePath = realpath($filePath);
438 7
        if (!array_key_exists($filePath, $this->imageCache)) {
439 7
            if (is_file($filePath)) {
440 7
                $image = \Swift_Image::fromPath($filePath);
441 7
                $id = $image->getId();
442
443
                // $id and $value must not be the same => this happens if the file cannot be found/read
444 7
                if ($id == $filePath) {
445
                    // @codeCoverageIgnoreStart
446
                    // add a null-value to the cache for this path, so we don't try again.
447
                    $this->imageCache[$filePath] = null;
448
                } else {
449
                    // @codeCoverageIgnoreEnd
450
                    // add the image to the cache
451 7
                    $this->imageCache[$filePath] = $image;
452
                }
453
            }
454
        }
455
456 7
        return $this->imageCache[$filePath];
457
    }
458
459
    /**
460
     * (non-PHPdoc).
461
     *
462
     * @see Azine\EmailBundle\Services.TemplateTwigSwiftMailerInterface::sendSingleEmail()
463
     *
464
     * @param string         $to
465
     * @param string         $toName
466
     * @param string         $subject
467
     * @param array          $params
468
     * @param string         $template
469
     * @param string         $emailLocale
470
     * @param null           $from
471
     * @param null           $fromName
472
     * @param \Swift_Message $message
473
     *
474
     * @return bool
475
     */
476 4
    public function sendSingleEmail($to, $toName, $subject, array $params, $template, $emailLocale, $from = null, $fromName = null, \Swift_Message &$message = null)
477
    {
478 4
        $failedRecipients = array();
479 4
        $this->sendEmail($failedRecipients, $subject, $from, $fromName, $to, $toName, null, null, null, null, null, null, $params, $template, array(), $emailLocale, $message);
480
481 4
        return 0 == sizeof($failedRecipients);
482
    }
483
484
    /**
485
     * Override the fosuserbundles original sendMessage, to embed template variables etc. into html-emails.
486
     *
487
     * @param string $templateName
488
     * @param array  $context
489
     * @param string $fromEmail
490
     * @param string $toEmail
491
     *
492
     * @return bool true if the mail was sent successfully, else false
493
     */
494 2
    protected function sendMessage($templateName, $context, $fromEmail, $toEmail)
495
    {
496
        // get the subject from the template
497
        // => make sure the subject block exists in your fos-templates (FOSUserBundle:Registration:email.txt.twig & FOSUserBundle:Resetting:email.txt.twig)
498 2
        $twigTemplate = $this->loadTemplate($templateName);
499 2
        $subject = $twigTemplate->renderBlock('subject', $context);
500
501 2
        return $this->sendSingleEmail($toEmail, null, $subject, $context, $templateName, $this->translator->getLocale(), $fromEmail);
502
    }
503
504
    /**
505
     * Return the Swift_Mailer to be used for sending mails immediately (e.g. instead of spooling them) if it is configured.
506
     *
507
     * @param $params
508
     *
509
     * @return \Swift_Mailer
510
     */
511 6
    private function getMailer($params)
512
    {
513
        // if the second mailer for immediate mail-delivery has been configured
514 6
        if (null !== $this->immediateMailer) {
515
            // check if this template has been configured to be sent immediately
516
            if (array_key_exists(AzineTemplateProvider::SEND_IMMEDIATELY_FLAG, $params) && $params[AzineTemplateProvider::SEND_IMMEDIATELY_FLAG]) {
517
                return $this->immediateMailer;
518
            }
519
        }
520
521 6
        return $this->mailer;
522
    }
523
}
524