Completed
Branch master (6e9dcf)
by Dominik
02:26
created

AzineTwigSwiftMailer::__construct()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 1

Importance

Changes 6
Bugs 1 Features 3
Metric Value
c 6
b 1
f 3
dl 0
loc 24
ccs 14
cts 14
cp 1
rs 8.9713
cc 1
eloc 22
nc 1
nop 10
crap 1

How to fix   Many Parameters   

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 Symfony\Component\Routing\RequestContext;
5
use Symfony\Component\HttpFoundation\File\Exception\FileException;
6
use Doctrine\ORM\EntityManager;
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 EntityManager
37
     */
38
    protected $entityManager;
39
40
    /**
41
     *
42
     * @var RequestContext
43
     */
44
    protected $routerContext;
45
46
    /**
47
     * @var email to use for "no-reply"
48
     */
49
    protected $noReplyEmail;
50
51
    /**
52
     * @var 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
    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 Logger                $logger
79
     * @param Translator            $translator
80
     * @param array                 $parameters
81
     * @param \Swift_Mailer         $immediateMailer
82
     */
83 7
    public function __construct(    \Swift_Mailer $mailer,
84
                                    UrlGeneratorInterface $router,
85
                                    \Twig_Environment $twig,
86
                                    Logger $logger,
87
                                    Translator $translator,
88
                                    TemplateProviderInterface $templateProvider,
89
                                    EntityManager $entityManager,
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $entityManager. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

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