Completed
Pull Request — master (#14)
by
unknown
04:02
created

AzineTwigSwiftMailer   F

Complexity

Total Complexity 58

Size/Duplication

Total Lines 499
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 98.46%

Importance

Changes 0
Metric Value
wmc 58
c 0
b 0
f 0
lcom 1
cbo 17
dl 0
loc 499
ccs 192
cts 195
cp 0.9846
rs 3.5483

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 23 1
D removeUnreferecedEmbededItemsFromMessage() 0 30 9
A loadTemplate() 0 8 2
C embedImages() 0 51 9
B cachedEmbedImage() 0 27 4
A sendSingleEmail() 0 7 1
A sendMessage() 0 9 1
A getMailer() 0 10 4
F sendEmail() 0 184 27

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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