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