Completed
Push — sendEmailChangeConfirmation ( 430b3c...f5bb7d )
by Dominik
06:40
created

AzineTwigSwiftMailer::sendEmail()   F

Complexity

Conditions 27
Paths > 20000

Size

Total Lines 184
Code Lines 89

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 106
CRAP Score 27

Importance

Changes 0
Metric Value
dl 0
loc 184
ccs 106
cts 106
cp 1
rs 2
c 0
b 0
f 0
cc 27
eloc 89
nc 332352
nop 17
crap 27

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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