AzineTwigSwiftMailer   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 524
Duplicated Lines 0 %

Test Coverage

Coverage 98.74%

Importance

Changes 14
Bugs 3 Features 6
Metric Value
eloc 176
c 14
b 3
f 6
dl 0
loc 524
ccs 157
cts 159
cp 0.9874
rs 4.08
wmc 59

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getMailer() 0 11 4
A __construct() 0 22 1
A sendMessage() 0 8 1
A sendUpdateEmailConfirmation() 0 10 1
A cachedEmbedImage() 0 22 4
B removeUnreferecedEmbededItemsFromMessage() 0 28 9
B embedImages() 0 47 9
A sendSingleEmail() 0 6 1
F sendEmail() 0 180 27
A loadTemplate() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like AzineTwigSwiftMailer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AzineTwigSwiftMailer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Azine\EmailBundle\Services;
4
5
use Azine\EmailBundle\DependencyInjection\AzineEmailExtension;
6
use Azine\EmailBundle\Entity\SentEmail;
7
use Azine\EmailUpdateConfirmationBundle\Mailer\EmailUpdateConfirmationMailerInterface;
8
use Doctrine\Common\Persistence\ManagerRegistry;
9
use FOS\UserBundle\Mailer\TwigSwiftMailer;
10
use FOS\UserBundle\Model\UserInterface;
11
use Symfony\Component\HttpFoundation\File\Exception\FileException;
12
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
13
use Symfony\Component\Routing\RequestContext;
14
use Symfony\Component\Translation\TranslatorInterface;
15
16
/**
17
 * This Service is used to send html-emails with embedded images.
18
 *
19
 * @author Dominik Businger
20
 */
21
class AzineTwigSwiftMailer extends TwigSwiftMailer implements TemplateTwigSwiftMailerInterface, EmailUpdateConfirmationMailerInterface
22
{
23
    /**
24
     * @var TranslatorInterface
25
     */
26
    protected $translator;
27
28
    /**
29
     * @var TemplateProviderInterface
30
     */
31
    protected $templateProvider;
32
33
    /**
34
     * @var ManagerRegistry
35
     */
36
    protected $managerRegistry;
37
38
    /**
39
     * @var RequestContext
40
     */
41
    protected $routerContext;
42
43
    /**
44
     * @var string email to use for "no-reply"
45
     */
46
    protected $noReplyEmail;
47
48
    /**
49
     * @var string name to use for "no-reply"
50
     */
51
    protected $noReplyName;
52
53
    /**
54
     * The Swift_Mailer to be used for sending emails immediately.
55
     *
56
     * @var \Swift_Mailer
57
     */
58
    private $immediateMailer;
59
60
    /**
61
     * @var EmailOpenTrackingCodeBuilderInterface
62
     */
63
    private $emailOpenTrackingCodeBuilder;
64
65
    /**
66
     * @var AzineEmailTwigExtension
67
     */
68
    private $emailTwigExtension;
69
70
    private $encodedItemIdPattern;
71
    private $templateCache = array();
72
    private $imageCache = array();
73
74
    /**
75
     * @param \Swift_Mailer                         $mailer
76
     * @param UrlGeneratorInterface                 $router
77
     * @param \Twig_Environment                     $twig
78
     * @param TranslatorInterface                   $translator
79
     * @param TemplateProviderInterface             $templateProvider
80
     * @param ManagerRegistry                       $managerRegistry
81
     * @param EmailOpenTrackingCodeBuilderInterface $emailOpenTrackingCodeBuilder
82
     * @param AzineEmailTwigExtension               $emailTwigExtension
83
     * @param array                                 $parameters
84 7
     * @param \Swift_Mailer                         $immediateMailer
85
     */
86
    public function __construct(\Swift_Mailer $mailer,
87
                                    UrlGeneratorInterface $router,
88
                                    \Twig_Environment $twig,
89
                                    TranslatorInterface $translator,
90
                                    TemplateProviderInterface $templateProvider,
91
                                    ManagerRegistry $managerRegistry,
92
                                    EmailOpenTrackingCodeBuilderInterface $emailOpenTrackingCodeBuilder,
93
                                    AzineEmailTwigExtension $emailTwigExtension,
94
                                    array $parameters,
95 7
                                    \Swift_Mailer $immediateMailer = null)
96 7
    {
97 7
        parent::__construct($mailer, $router, $twig, $parameters);
98 7
        $this->immediateMailer = $immediateMailer;
99 7
        $this->translator = $translator;
100 7
        $this->templateProvider = $templateProvider;
101 7
        $this->managerRegistry = $managerRegistry;
102 7
        $this->noReplyEmail = $parameters[AzineEmailExtension::NO_REPLY][AzineEmailExtension::NO_REPLY_EMAIL_ADDRESS];
103 7
        $this->noReplyName = $parameters[AzineEmailExtension::NO_REPLY][AzineEmailExtension::NO_REPLY_EMAIL_NAME];
104 7
        $this->emailOpenTrackingCodeBuilder = $emailOpenTrackingCodeBuilder;
105 7
        $this->routerContext = $router->getContext();
106 7
        $this->encodedItemIdPattern = '/^cid:.*@/';
107
        $this->emailTwigExtension = $emailTwigExtension;
108
    }
109
110
    /**
111
     * (non-PHPdoc).
112
     *
113
     * @see Azine\EmailBundle\Services.TemplateTwigSwiftMailerInterface::sendEmail()
114
     *
115
     * @param array        $failedRecipients
116
     * @param string       $subject
117
     * @param string       $from
118
     * @param string       $fromName
119
     * @param array|string $to
120
     * @param string       $toName
121
     * @param array|string $cc
122
     * @param string       $ccName
123
     * @param array|string $bcc
124
     * @param string       $bccName
125
     * @param $replyTo
126
     * @param $replyToName
127
     * @param array $params
128
     * @param $template
129
     * @param array          $attachments
130
     * @param null           $emailLocale
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $emailLocale is correct as it would always require null to be passed?
Loading history...
131
     * @param \Swift_Message $message
132
     *
133 7
     * @return int
134
     */
135
    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)
136 7
    {
137 7
        // create the message
138
        if (null === $message) {
139
            $message = new \Swift_Message();
140 7
        }
141
142
        $message->setSubject($subject);
143 7
144 2
        // set the from-Name & -Email to the default ones if not given
145 2
        if (null === $from) {
0 ignored issues
show
introduced by
The condition null === $from is always false.
Loading history...
146 2
            $from = $this->noReplyEmail;
147
            if (null === $fromName) {
148
                $fromName = $this->noReplyName;
149
            }
150
        }
151 7
152 7
        // add the from-email for the footer-text
153 7
        if (!array_key_exists('fromEmail', $params)) {
154
            $params['sendMailAccountName'] = $this->noReplyName;
155
            $params['sendMailAccountAddress'] = $this->noReplyEmail;
156
        }
157 7
158
        // get the baseTemplate. => templateId without the ending.
159
        $templateBaseId = substr($template, 0, strrpos($template, '.', -6));
160 7
161
        // check if this email should be stored for web-view
162 4
        if ($this->templateProvider->saveWebViewFor($templateBaseId)) {
163
            // keep a copy of the vars for the web-view
164
            $webViewParams = $params;
165 4
166
            // add the web-view token
167 3
            $params[$this->templateProvider->getWebViewTokenId()] = SentEmail::getNewToken();
168
        } else {
169
            $webViewParams = array();
170
        }
171 7
172
        // recursively add all template-variables for the wrapper-templates and contentItems
173
        $params = $this->templateProvider->addTemplateVariablesFor($templateBaseId, $params);
174 7
175
        // recursively attach all messages in the array
176
        $this->embedImages($message, $params);
177 7
178 6
        // change the locale for the email-recipients
179
        if (null !== $emailLocale && strlen($emailLocale) > 0) {
0 ignored issues
show
introduced by
The condition null !== $emailLocale is always false.
Loading history...
Bug introduced by
$emailLocale of type void is incompatible with the type string expected by parameter $string of strlen(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

179
        if (null !== $emailLocale && strlen(/** @scrutinizer ignore-type */ $emailLocale) > 0) {
Loading history...
180
            $currentUserLocale = $this->translator->getLocale();
181 6
182
            // change the router-context locale
183
            $this->routerContext->setParameter('_locale', $emailLocale);
184 6
185
            // change the translator locale
186 1
            $this->translator->setLocale($emailLocale);
187
        } else {
188
            $emailLocale = $this->translator->getLocale();
189
        }
190 7
191
        // recursively add snippets for the wrapper-templates and contentItems
192
        $params = $this->templateProvider->addTemplateSnippetsWithImagesFor($templateBaseId, $params, $emailLocale);
193 7
194
        // add the emailLocale (used for web-view)
195
        $params['emailLocale'] = $emailLocale;
196 7
197 7
        // render the email parts
198 7
        $twigTemplate = $this->loadTemplate($template);
199
        $textBody = $twigTemplate->renderBlock('body_text', $params);
200 7
        $message->addPart($textBody, 'text/plain');
201
202 7
        $htmlBody = $twigTemplate->renderBlock('body_html', $params);
203
204 7
        $campaignParams = $this->templateProvider->getCampaignParamsFor($templateBaseId, $params);
205 5
206
        if (sizeof($campaignParams) > 0) {
207
            $htmlBody = $this->emailTwigExtension->addCampaignParamsToAllUrls($htmlBody, $campaignParams);
208
        }
209 7
210
        // if email-tracking is enabled
211 7
        if ($this->emailOpenTrackingCodeBuilder) {
212 7
            // add an image at the end of the html tag with the tracking-params to track email-opens
213 5
            $imgTrackingCode = $this->emailOpenTrackingCodeBuilder->getTrackingImgCode($templateBaseId, $campaignParams, $params, $message->getId(), $to, $cc, $bcc);
214 5
            if ($imgTrackingCode && strlen($imgTrackingCode) > 0) {
215
                $htmlCloseTagPosition = strpos($htmlBody, '</body>');
216
                $htmlBody = substr_replace($htmlBody, $imgTrackingCode, $htmlCloseTagPosition, 0);
217
            }
218 7
        }
219
220
        $message->setBody($htmlBody, 'text/html');
221 7
222
        // remove unused/unreferenced embeded items from the message
223
        $message = $this->removeUnreferecedEmbededItemsFromMessage($message, $params, $htmlBody);
224 7
225 6
        // change the locale back to the users locale
226 6
        if (isset($currentUserLocale) && null !== $currentUserLocale) {
227
            $this->routerContext->setParameter('_locale', $currentUserLocale);
228
            $this->translator->setLocale($currentUserLocale);
229
        }
230 7
231
        // add attachments
232 2
        foreach ($attachments as $fileName => $file) {
233
            // add attachment from existing file
234 2
            if (is_string($file)) {
235 1
                // check that the file really exists!
236 1
                if (file_exists($file)) {
237 1
                    $attachment = \Swift_Attachment::fromPath($file);
238
                    if (strlen($fileName) >= 5) {
239
                        $attachment->setFilename($fileName);
240 2
                    }
241
                } else {
242
                    throw new FileException('File not found: '.$file);
243
                }
244
245 1
                // add attachment from generated data
246
            } else {
247
                $attachment = new \Swift_Attachment($file, $fileName);
248 1
            }
249
250
            $message->attach($attachment);
251
        }
252 6
253 6
        // set the addresses
254
        if ($from) {
255 6
            $message->setFrom($from, $fromName);
256 2
        }
257 4
        if ($replyTo) {
258 4
            $message->setReplyTo($replyTo, $replyToName);
259
        } elseif ($from) {
260 6
            $message->setReplyTo($from, $fromName);
261 6
        }
262
        if ($to) {
263 6
            $message->setTo($to, $toName);
264 2
        }
265
        if ($cc) {
266 6
            $message->setCc($cc, $ccName);
267 2
        }
268
        if ($bcc) {
269
            $message->setBcc($bcc, $bccName);
270
        }
271 6
272
        // add custom headers
273
        $this->templateProvider->addCustomHeaders($templateBaseId, $message, $params);
274 6
275 6
        // send the message
276
        $mailer = $this->getMailer($params);
277
        $messagesSent = $mailer->send($message, $failedRecipients);
278
279 6
        // if the message was successfully sent,
280
        // and it should be made available in web-view
281 2
        if ($messagesSent && array_key_exists($this->templateProvider->getWebViewTokenId(), $params)) {
282 2
            // store the email
283 2
            $sentEmail = new SentEmail();
284 2
            $sentEmail->setToken($params[$this->templateProvider->getWebViewTokenId()]);
285
            $sentEmail->setTemplate($templateBaseId);
286
            $sentEmail->setSent(new \DateTime());
287 2
288
            // recursively add all template-variables for the wrapper-templates and contentItems
289
            $webViewParams = $this->templateProvider->addTemplateVariablesFor($template, $webViewParams);
290 2
291
            // replace absolute image-paths with relative ones.
292
            $webViewParams = $this->templateProvider->makeImagePathsWebRelative($webViewParams, $emailLocale);
293 2
294
            // recursively add snippets for the wrapper-templates and contentItems
295 2
            $webViewParams = $this->templateProvider->addTemplateSnippetsWithImagesFor($template, $webViewParams, $emailLocale, true);
296
297
            $sentEmail->setVariables($webViewParams);
298 2
299 2
            // save only successfull recipients
300
            if (!is_array($to)) {
301 2
                $to = array($to);
302 2
            }
303
            $successfulRecipients = array_diff($to, $failedRecipients);
304
            $sentEmail->setRecipients($successfulRecipients);
305 2
306 2
            // write to db
307 2
            $em = $this->managerRegistry->getManager();
308 2
            $em->persist($sentEmail);
309 2
            $em->flush($sentEmail);
310
            $em->clear();
311
            gc_collect_cycles();
312 6
        }
313
314
        return $messagesSent;
315
    }
316
317
    /**
318
     * Remove all Embeded Attachments that are not referenced in the html-body from the message
319
     * to avoid using unneccary bandwidth.
320
     *
321
     * @param \Swift_Message $message
322
     * @param array          $params   the parameters used to render the html
323
     * @param string         $htmlBody
324
     *
325 7
     * @return \Swift_Message
326
     */
327 7
    private function removeUnreferecedEmbededItemsFromMessage(\Swift_Message $message, $params, $htmlBody)
328
    {
329 7
        foreach ($params as $key => $value) {
330 2
            // remove unreferenced attachments from contentItems too.
331 2
            if ('contentItems' === $key) {
332
                foreach ($value as $contentItemParams) {
333
                    $message = $this->removeUnreferecedEmbededItemsFromMessage($message, $contentItemParams, $htmlBody);
334
                }
335 7
            } else {
336
                // check if the embeded items are referenced in the templates
337 7
                $isEmbededItem = is_string($value) && 1 == preg_match($this->encodedItemIdPattern, $value);
338
339 7
                if ($isEmbededItem && false === stripos($htmlBody, $value)) {
340
                    // remove unreferenced items
341 7
                    $children = array();
342 7
343 7
                    foreach ($message->getChildren() as $attachment) {
344
                        if ('cid:'.$attachment->getId() != $value) {
345
                            $children[] = $attachment;
346
                        }
347 7
                    }
348
349
                    $message->setChildren($children);
350
                }
351
            }
352 7
        }
353
354
        return $message;
355
    }
356
357
    /**
358
     * Get the template from the cache if it was loaded already.
359
     *
360
     * @param string $template
361
     *
362 7
     * @return \Twig_Template
363
     */
364 7
    private function loadTemplate($template)
365 7
    {
366
        if (!array_key_exists($template, $this->templateCache)) {
367
            $this->templateCache[$template] = $this->twig->loadTemplate($template);
368 7
        }
369
370
        return $this->templateCache[$template];
371
    }
372
373
    /**
374
     * Recursively embed all images in the array into the message.
375
     *
376
     * @param \Swift_Message $message
377
     * @param array          $params
378
     *
379 7
     * @return array $params
380
     */
381
    private function embedImages(&$message, &$params)
382 7
    {
383
        // loop through the array
384 7
        foreach ($params as $key => $value) {
385
            // if the current value is an array
386 2
            if (is_array($value)) {
387 2
                // search for more images deeper in the arrays
388
                $value = $this->embedImages($message, $value);
389
                $params[$key] = $value;
390 7
391 7
            // if the current value is an existing file from the image-folder, embed it
392
            } elseif (is_string($value)) {
393 7
                if (is_file($value)) {
394 7
                    // check if the file is from an allowed folder
395 7
                    if (false !== $this->templateProvider->isFileAllowed($value)) {
396 7
                        $encodedImage = $this->cachedEmbedImage($value);
397 7
                        if (null !== $encodedImage) {
398
                            $id = $message->embed($encodedImage);
399
                            $params[$key] = $id;
400
                        }
401
                    }
402
403
                    // the $filePath isn't a regular file
404 7
                } else {
405
                    // add a null-value to the cache for this path, so we don't try again.
406
                    $this->imageCache[$value] = null;
407
                }
408 7
409
                //if the current value is a generated image
410 1
            } elseif (is_resource($value) && 0 == stripos(get_resource_type($value), 'gd')) {
411 1
                // get the image-data as string
412 1
                ob_start();
413
                imagepng($value);
414
                $imageData = ob_get_clean();
415 1
416 1
                // encode the image
417 7
                $encodedImage = new \Swift_Image($imageData, 'generatedImage'.md5($imageData));
418
                $id = $message->embed($encodedImage);
419
                $params[$key] = $id;
420
            }
421
            // don't do anything
422
        }
423 7
424
        // remove duplicate-attachments
425 7
        $message->setChildren(array_unique($message->getChildren()));
426
427
        return $params;
428
    }
429
430
    /**
431
     * Get the Swift_Image for the file.
432
     *
433
     * @param string $filePath
434
     *
435 7
     * @return \Swift_Image|null
436
     */
437 7
    private function cachedEmbedImage($filePath)
438 7
    {
439 7
        $filePath = realpath($filePath);
440 7
        if (!array_key_exists($filePath, $this->imageCache)) {
441 7
            if (is_file($filePath)) {
442
                $image = \Swift_Image::fromPath($filePath);
443
                $id = $image->getId();
444 7
445
                // $id and $value must not be the same => this happens if the file cannot be found/read
446
                if ($id == $filePath) {
447
                    // @codeCoverageIgnoreStart
448
                    // add a null-value to the cache for this path, so we don't try again.
449
                    $this->imageCache[$filePath] = null;
450
                } else {
451 7
                    // @codeCoverageIgnoreEnd
452
                    // add the image to the cache
453
                    $this->imageCache[$filePath] = $image;
454
                }
455
            }
456 7
        }
457
458
        return $this->imageCache[$filePath];
459
    }
460
461
    /**
462
     * (non-PHPdoc).
463
     *
464
     * @see Azine\EmailBundle\Services.TemplateTwigSwiftMailerInterface::sendSingleEmail()
465
     *
466
     * @param string         $to
467
     * @param string         $toName
468
     * @param string         $subject
469
     * @param array          $params
470
     * @param string         $template
471
     * @param string         $emailLocale
472
     * @param null           $from
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $from is correct as it would always require null to be passed?
Loading history...
473
     * @param null           $fromName
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $fromName is correct as it would always require null to be passed?
Loading history...
474
     * @param \Swift_Message $message
475
     *
476 4
     * @return bool
477
     */
478 4
    public function sendSingleEmail($to, $toName, $subject, array $params, $template, $emailLocale, $from = null, $fromName = null, \Swift_Message &$message = null)
479 4
    {
480
        $failedRecipients = array();
481 4
        $this->sendEmail($failedRecipients, $subject, $from, $fromName, $to, $toName, null, null, null, null, null, null, $params, $template, array(), $emailLocale, $message);
482
483
        return 0 == sizeof($failedRecipients);
484
    }
485
486
    /**
487
     * Override the fosuserbundles original sendMessage, to embed template variables etc. into html-emails.
488
     *
489
     * @param string $templateName
490
     * @param array  $context
491
     * @param string $fromEmail
492
     * @param string $toEmail
493
     *
494 2
     * @return bool true if the mail was sent successfully, else false
495
     */
496
    protected function sendMessage($templateName, $context, $fromEmail, $toEmail)
497
    {
498 2
        // get the subject from the template
499 2
        // => make sure the subject block exists in your fos-templates (FOSUserBundle:Registration:email.txt.twig & FOSUserBundle:Resetting:email.txt.twig)
500
        $twigTemplate = $this->loadTemplate($templateName);
501 2
        $subject = $twigTemplate->renderBlock('subject', $context);
502
503
        return $this->sendSingleEmail($toEmail, null, $subject, $context, $templateName, $this->translator->getLocale(), $fromEmail);
504
    }
505
506
    /**
507
     * Return the Swift_Mailer to be used for sending mails immediately (e.g. instead of spooling them) if it is configured.
508
     *
509
     * @param $params
510
     *
511 6
     * @return \Swift_Mailer
512
     */
513
    private function getMailer($params)
514 6
    {
515
        // if the second mailer for immediate mail-delivery has been configured
516
        if (null !== $this->immediateMailer) {
517
            // check if this template has been configured to be sent immediately
518
            if (array_key_exists(AzineTemplateProvider::SEND_IMMEDIATELY_FLAG, $params) && $params[AzineTemplateProvider::SEND_IMMEDIATELY_FLAG]) {
519
                return $this->immediateMailer;
520
            }
521 6
        }
522
523
        return $this->mailer;
524
    }
525
526
    /**
527
     * Send confirmation link to specified new user email.
528
     *
529
     * @param UserInterface $user
530
     * @param $confirmationUrl
531
     * @param $toEmail
532
     *
533
     * @return bool
534
     */
535
    public function sendUpdateEmailConfirmation(UserInterface $user, $confirmationUrl, $toEmail)
536
    {
537
        $template = $this->parameters['template']['email_updating'];
538
        $fromEmail = $this->parameters['from_email']['confirmation'];
539
        $context = array(
540
            'user' => $user,
541
            'confirmationUrl' => $confirmationUrl,
542
        );
543
544
        $this->sendMessage($template, $context, $fromEmail, $toEmail);
545
    }
546
}
547