Completed
Push — master ( 06cda5...85418c )
by Dominik
03:45
created

AzineTwigSwiftMailer::embedImages()   C

Complexity

Conditions 11
Paths 9

Size

Total Lines 56
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 11.0359

Importance

Changes 0
Metric Value
dl 0
loc 56
ccs 28
cts 30
cp 0.9333
rs 6.5481
c 0
b 0
f 0
cc 11
eloc 26
nc 9
nop 2
crap 11.0359

How to fix   Long Method    Complexity   

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