Completed
Push — fix-phpunit-inclusion ( 32fa89 )
by Dominik
14:50
created

AzineTwigSwiftMailer::embedImages()   C

Complexity

Conditions 9
Paths 8

Size

Total Lines 51
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 51
ccs 27
cts 27
cp 1
rs 6.2727
c 0
b 0
f 0
cc 9
eloc 24
nc 8
nop 2
crap 9

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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