AzineTwigSwiftMailer::sendSingleEmail()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 9
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 1
rs 10

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
3
namespace Azine\EmailBundle\Services;
4
5
use Azine\EmailBundle\DependencyInjection\AzineEmailExtension;
6
use Azine\EmailBundle\Entity\SentEmail;
7
use Azine\EmailUpdateConfirmationBundle\Mailer\EmailUpdateConfirmationMailerInterface;
8
use Doctrine\Common\Persistence\ManagerRegistry;
9
use FOS\UserBundle\Mailer\TwigSwiftMailer;
10
use FOS\UserBundle\Model\UserInterface;
11
use Symfony\Component\HttpFoundation\File\Exception\FileException;
12
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
13
use Symfony\Component\Routing\RequestContext;
14
use Symfony\Component\Translation\TranslatorInterface;
15
16
/**
17
 * This Service is used to send html-emails with embedded images.
18
 *
19
 * @author Dominik Businger
20
 */
21
class AzineTwigSwiftMailer extends TwigSwiftMailer implements TemplateTwigSwiftMailerInterface, EmailUpdateConfirmationMailerInterface
22
{
23
    /**
24
     * @var TranslatorInterface
25
     */
26
    protected $translator;
27
28
    /**
29
     * @var TemplateProviderInterface
30
     */
31
    protected $templateProvider;
32
33
    /**
34
     * @var ManagerRegistry
35
     */
36
    protected $managerRegistry;
37
38
    /**
39
     * @var RequestContext
40
     */
41
    protected $routerContext;
42
43
    /**
44
     * @var string email to use for "no-reply"
45
     */
46
    protected $noReplyEmail;
47
48
    /**
49
     * @var string name to use for "no-reply"
50
     */
51
    protected $noReplyName;
52
53
    /**
54
     * The Swift_Mailer to be used for sending emails immediately.
55
     *
56
     * @var \Swift_Mailer
57
     */
58
    private $immediateMailer;
59
60
    /**
61
     * @var EmailOpenTrackingCodeBuilderInterface
62
     */
63
    private $emailOpenTrackingCodeBuilder;
64
65
    /**
66
     * @var AzineEmailTwigExtension
67
     */
68
    private $emailTwigExtension;
69
70
    private $encodedItemIdPattern;
71
    private $templateCache = array();
72
    private $imageCache = array();
73
74
    /**
75
     * @param \Swift_Mailer                         $mailer
76
     * @param UrlGeneratorInterface                 $router
77
     * @param \Twig_Environment                     $twig
78
     * @param TranslatorInterface                   $translator
79
     * @param TemplateProviderInterface             $templateProvider
80
     * @param ManagerRegistry                       $managerRegistry
81
     * @param EmailOpenTrackingCodeBuilderInterface $emailOpenTrackingCodeBuilder
82
     * @param AzineEmailTwigExtension               $emailTwigExtension
83
     * @param array                                 $parameters
84 7
     * @param \Swift_Mailer                         $immediateMailer
85
     */
86
    public function __construct(\Swift_Mailer $mailer,
87
                                    UrlGeneratorInterface $router,
88
                                    \Twig_Environment $twig,
89
                                    TranslatorInterface $translator,
90
                                    TemplateProviderInterface $templateProvider,
91
                                    ManagerRegistry $managerRegistry,
92
                                    EmailOpenTrackingCodeBuilderInterface $emailOpenTrackingCodeBuilder,
93
                                    AzineEmailTwigExtension $emailTwigExtension,
94
                                    array $parameters,
95 7
                                    \Swift_Mailer $immediateMailer = null)
96 7
    {
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->encodedItemIdPattern = '/^cid:.*@/';
107
        $this->emailTwigExtension = $emailTwigExtension;
108
    }
109
110
    /**
111
     * (non-PHPdoc).
112
     *
113
     * @see Azine\EmailBundle\Services.TemplateTwigSwiftMailerInterface::sendEmail()
114
     *
115
     * @param array        $failedRecipients
116
     * @param string       $subject
117
     * @param string       $from
118
     * @param string       $fromName
119
     * @param array|string $to
120
     * @param string       $toName
121
     * @param array|string $cc
122
     * @param string       $ccName
123
     * @param array|string $bcc
124
     * @param string       $bccName
125
     * @param $replyTo
126
     * @param $replyToName
127
     * @param array $params
128
     * @param $template
129
     * @param array          $attachments
130
     * @param null           $emailLocale
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $emailLocale is correct as it would always require null to be passed?
Loading history...
131
     * @param \Swift_Message $message
132
     *
133 7
     * @return int
134
     */
135
    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)
136 7
    {
137 7
        // create the message
138
        if (null === $message) {
139
            $message = new \Swift_Message();
140 7
        }
141
142
        $message->setSubject($subject);
143 7
144 2
        // set the from-Name & -Email to the default ones if not given
145 2
        if (null === $from) {
0 ignored issues
show
introduced by
The condition null === $from is always false.
Loading history...
146 2
            $from = $this->noReplyEmail;
147
            if (null === $fromName) {
148
                $fromName = $this->noReplyName;
149
            }
150
        }
151 7
152 7
        // add the from-email for the footer-text
153 7
        if (!array_key_exists('fromEmail', $params)) {
154
            $params['sendMailAccountName'] = $this->noReplyName;
155
            $params['sendMailAccountAddress'] = $this->noReplyEmail;
156
        }
157 7
158
        // get the baseTemplate. => templateId without the ending.
159
        $templateBaseId = substr($template, 0, strrpos($template, '.', -6));
160 7
161
        // check if this email should be stored for web-view
162 4
        if ($this->templateProvider->saveWebViewFor($templateBaseId)) {
163
            // keep a copy of the vars for the web-view
164
            $webViewParams = $params;
165 4
166
            // add the web-view token
167 3
            $params[$this->templateProvider->getWebViewTokenId()] = SentEmail::getNewToken();
168
        } else {
169
            $webViewParams = array();
170
        }
171 7
172
        // recursively add all template-variables for the wrapper-templates and contentItems
173
        $params = $this->templateProvider->addTemplateVariablesFor($templateBaseId, $params);
174 7
175
        // recursively attach all messages in the array
176
        $this->embedImages($message, $params);
177 7
178 6
        // change the locale for the email-recipients
179
        if (null !== $emailLocale && strlen($emailLocale) > 0) {
0 ignored issues
show
introduced by
The condition null !== $emailLocale is always false.
Loading history...
Bug introduced by
$emailLocale of type void is incompatible with the type string expected by parameter $string of strlen(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

179
        if (null !== $emailLocale && strlen(/** @scrutinizer ignore-type */ $emailLocale) > 0) {
Loading history...
180
            $currentUserLocale = $this->translator->getLocale();
181 6
182
            // change the router-context locale
183
            $this->routerContext->setParameter('_locale', $emailLocale);
184 6
185
            // change the translator locale
186 1
            $this->translator->setLocale($emailLocale);
187
        } else {
188
            $emailLocale = $this->translator->getLocale();
189
        }
190 7
191
        // recursively add snippets for the wrapper-templates and contentItems
192
        $params = $this->templateProvider->addTemplateSnippetsWithImagesFor($templateBaseId, $params, $emailLocale);
193 7
194
        // add the emailLocale (used for web-view)
195
        $params['emailLocale'] = $emailLocale;
196 7
197 7
        // render the email parts
198 7
        $twigTemplate = $this->loadTemplate($template);
199
        $textBody = $twigTemplate->renderBlock('body_text', $params);
200 7
        $message->addPart($textBody, 'text/plain');
201
202 7
        $htmlBody = $twigTemplate->renderBlock('body_html', $params);
203
204 7
        $campaignParams = $this->templateProvider->getCampaignParamsFor($templateBaseId, $params);
205 5
206
        if (sizeof($campaignParams) > 0) {
207
            $htmlBody = $this->emailTwigExtension->addCampaignParamsToAllUrls($htmlBody, $campaignParams);
208
        }
209 7
210
        // if email-tracking is enabled
211 7
        if ($this->emailOpenTrackingCodeBuilder) {
212 7
            // add an image at the end of the html tag with the tracking-params to track email-opens
213 5
            $imgTrackingCode = $this->emailOpenTrackingCodeBuilder->getTrackingImgCode($templateBaseId, $campaignParams, $params, $message->getId(), $to, $cc, $bcc);
214 5
            if ($imgTrackingCode && strlen($imgTrackingCode) > 0) {
215
                $htmlCloseTagPosition = strpos($htmlBody, '</body>');
216
                $htmlBody = substr_replace($htmlBody, $imgTrackingCode, $htmlCloseTagPosition, 0);
217
            }
218 7
        }
219
220
        $message->setBody($htmlBody, 'text/html');
221 7
222
        // remove unused/unreferenced embeded items from the message
223
        $message = $this->removeUnreferecedEmbededItemsFromMessage($message, $params, $htmlBody);
224 7
225 6
        // change the locale back to the users locale
226 6
        if (isset($currentUserLocale) && null !== $currentUserLocale) {
227
            $this->routerContext->setParameter('_locale', $currentUserLocale);
228
            $this->translator->setLocale($currentUserLocale);
229
        }
230 7
231
        // add attachments
232 2
        foreach ($attachments as $fileName => $file) {
233
            // add attachment from existing file
234 2
            if (is_string($file)) {
235 1
                // check that the file really exists!
236 1
                if (file_exists($file)) {
237 1
                    $attachment = \Swift_Attachment::fromPath($file);
238
                    if (strlen($fileName) >= 5) {
239
                        $attachment->setFilename($fileName);
240 2
                    }
241
                } else {
242
                    throw new FileException('File not found: '.$file);
243
                }
244
245 1
                // add attachment from generated data
246
            } else {
247
                $attachment = new \Swift_Attachment($file, $fileName);
248 1
            }
249
250
            $message->attach($attachment);
251
        }
252 6
253 6
        // set the addresses
254
        if ($from) {
255 6
            $message->setFrom($from, $fromName);
256 2
        }
257 4
        if ($replyTo) {
258 4
            $message->setReplyTo($replyTo, $replyToName);
259
        } elseif ($from) {
260 6
            $message->setReplyTo($from, $fromName);
261 6
        }
262
        if ($to) {
263 6
            $message->setTo($to, $toName);
264 2
        }
265
        if ($cc) {
266 6
            $message->setCc($cc, $ccName);
267 2
        }
268
        if ($bcc) {
269
            $message->setBcc($bcc, $bccName);
270
        }
271 6
272
        // add custom headers
273
        $this->templateProvider->addCustomHeaders($templateBaseId, $message, $params);
274 6
275 6
        // send the message
276
        $mailer = $this->getMailer($params);
277
        $messagesSent = $mailer->send($message, $failedRecipients);
278
279 6
        // if the message was successfully sent,
280
        // and it should be made available in web-view
281 2
        if ($messagesSent && array_key_exists($this->templateProvider->getWebViewTokenId(), $params)) {
282 2
            // store the email
283 2
            $sentEmail = new SentEmail();
284 2
            $sentEmail->setToken($params[$this->templateProvider->getWebViewTokenId()]);
285
            $sentEmail->setTemplate($templateBaseId);
286
            $sentEmail->setSent(new \DateTime());
287 2
288
            // recursively add all template-variables for the wrapper-templates and contentItems
289
            $webViewParams = $this->templateProvider->addTemplateVariablesFor($template, $webViewParams);
290 2
291
            // replace absolute image-paths with relative ones.
292
            $webViewParams = $this->templateProvider->makeImagePathsWebRelative($webViewParams, $emailLocale);
293 2
294
            // recursively add snippets for the wrapper-templates and contentItems
295 2
            $webViewParams = $this->templateProvider->addTemplateSnippetsWithImagesFor($template, $webViewParams, $emailLocale, true);
296
297
            $sentEmail->setVariables($webViewParams);
298 2
299 2
            // save only successfull recipients
300
            if (!is_array($to)) {
301 2
                $to = array($to);
302 2
            }
303
            $successfulRecipients = array_diff($to, $failedRecipients);
304
            $sentEmail->setRecipients($successfulRecipients);
305 2
306 2
            // write to db
307 2
            $em = $this->managerRegistry->getManager();
308 2
            $em->persist($sentEmail);
309 2
            $em->flush($sentEmail);
310
            $em->clear();
311
            gc_collect_cycles();
312 6
        }
313
314
        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
     *
325 7
     * @return \Swift_Message
326
     */
327 7
    private function removeUnreferecedEmbededItemsFromMessage(\Swift_Message $message, $params, $htmlBody)
328
    {
329 7
        foreach ($params as $key => $value) {
330 2
            // remove unreferenced attachments from contentItems too.
331 2
            if ('contentItems' === $key) {
332
                foreach ($value as $contentItemParams) {
333
                    $message = $this->removeUnreferecedEmbededItemsFromMessage($message, $contentItemParams, $htmlBody);
334
                }
335 7
            } else {
336
                // check if the embeded items are referenced in the templates
337 7
                $isEmbededItem = is_string($value) && 1 == preg_match($this->encodedItemIdPattern, $value);
338
339 7
                if ($isEmbededItem && false === stripos($htmlBody, $value)) {
340
                    // remove unreferenced items
341 7
                    $children = array();
342 7
343 7
                    foreach ($message->getChildren() as $attachment) {
344
                        if ('cid:'.$attachment->getId() != $value) {
345
                            $children[] = $attachment;
346
                        }
347 7
                    }
348
349
                    $message->setChildren($children);
350
                }
351
            }
352 7
        }
353
354
        return $message;
355
    }
356
357
    /**
358
     * Get the template from the cache if it was loaded already.
359
     *
360
     * @param string $template
361
     *
362 7
     * @return \Twig_Template
363
     */
364 7
    private function loadTemplate($template)
365 7
    {
366
        if (!array_key_exists($template, $this->templateCache)) {
367
            $this->templateCache[$template] = $this->twig->loadTemplate($template);
368 7
        }
369
370
        return $this->templateCache[$template];
371
    }
372
373
    /**
374
     * Recursively embed all images in the array into the message.
375
     *
376
     * @param \Swift_Message $message
377
     * @param array          $params
378
     *
379 7
     * @return array $params
380
     */
381
    private function embedImages(&$message, &$params)
382 7
    {
383
        // loop through the array
384 7
        foreach ($params as $key => $value) {
385
            // if the current value is an array
386 2
            if (is_array($value)) {
387 2
                // search for more images deeper in the arrays
388
                $value = $this->embedImages($message, $value);
389
                $params[$key] = $value;
390 7
391 7
            // if the current value is an existing file from the image-folder, embed it
392
            } elseif (is_string($value)) {
393 7
                if (is_file($value)) {
394 7
                    // check if the file is from an allowed folder
395 7
                    if (false !== $this->templateProvider->isFileAllowed($value)) {
396 7
                        $encodedImage = $this->cachedEmbedImage($value);
397 7
                        if (null !== $encodedImage) {
398
                            $id = $message->embed($encodedImage);
399
                            $params[$key] = $id;
400
                        }
401
                    }
402
403
                    // the $filePath isn't a regular file
404 7
                } else {
405
                    // add a null-value to the cache for this path, so we don't try again.
406
                    $this->imageCache[$value] = null;
407
                }
408 7
409
                //if the current value is a generated image
410 1
            } elseif (is_resource($value) && 0 == stripos(get_resource_type($value), 'gd')) {
411 1
                // get the image-data as string
412 1
                ob_start();
413
                imagepng($value);
414
                $imageData = ob_get_clean();
415 1
416 1
                // encode the image
417 7
                $encodedImage = new \Swift_Image($imageData, 'generatedImage'.md5($imageData));
418
                $id = $message->embed($encodedImage);
419
                $params[$key] = $id;
420
            }
421
            // don't do anything
422
        }
423 7
424
        // remove duplicate-attachments
425 7
        $message->setChildren(array_unique($message->getChildren()));
426
427
        return $params;
428
    }
429
430
    /**
431
     * Get the Swift_Image for the file.
432
     *
433
     * @param string $filePath
434
     *
435 7
     * @return \Swift_Image|null
436
     */
437 7
    private function cachedEmbedImage($filePath)
438 7
    {
439 7
        $filePath = realpath($filePath);
440 7
        if (!array_key_exists($filePath, $this->imageCache)) {
441 7
            if (is_file($filePath)) {
442
                $image = \Swift_Image::fromPath($filePath);
443
                $id = $image->getId();
444 7
445
                // $id and $value must not be the same => this happens if the file cannot be found/read
446
                if ($id == $filePath) {
447
                    // @codeCoverageIgnoreStart
448
                    // add a null-value to the cache for this path, so we don't try again.
449
                    $this->imageCache[$filePath] = null;
450
                } else {
451 7
                    // @codeCoverageIgnoreEnd
452
                    // add the image to the cache
453
                    $this->imageCache[$filePath] = $image;
454
                }
455
            }
456 7
        }
457
458
        return $this->imageCache[$filePath];
459
    }
460
461
    /**
462
     * (non-PHPdoc).
463
     *
464
     * @see Azine\EmailBundle\Services.TemplateTwigSwiftMailerInterface::sendSingleEmail()
465
     *
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
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $from is correct as it would always require null to be passed?
Loading history...
473
     * @param null           $fromName
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $fromName is correct as it would always require null to be passed?
Loading history...
474
     * @param \Swift_Message $message
475
     *
476 4
     * @return bool
477
     */
478 4
    public function sendSingleEmail($to, $toName, $subject, array $params, $template, $emailLocale, $from = null, $fromName = null, \Swift_Message &$message = null)
479 4
    {
480
        $failedRecipients = array();
481 4
        $this->sendEmail($failedRecipients, $subject, $from, $fromName, $to, $toName, null, null, null, null, null, null, $params, $template, array(), $emailLocale, $message);
482
483
        return 0 == sizeof($failedRecipients);
484
    }
485
486
    /**
487
     * Override the fosuserbundles original sendMessage, to embed template variables etc. into html-emails.
488
     *
489
     * @param string $templateName
490
     * @param array  $context
491
     * @param string $fromEmail
492
     * @param string $toEmail
493
     *
494 2
     * @return bool true if the mail was sent successfully, else false
495
     */
496
    protected function sendMessage($templateName, $context, $fromEmail, $toEmail)
497
    {
498 2
        // get the subject from the template
499 2
        // => make sure the subject block exists in your fos-templates (FOSUserBundle:Registration:email.txt.twig & FOSUserBundle:Resetting:email.txt.twig)
500
        $twigTemplate = $this->loadTemplate($templateName);
501 2
        $subject = $twigTemplate->renderBlock('subject', $context);
502
503
        return $this->sendSingleEmail($toEmail, null, $subject, $context, $templateName, $this->translator->getLocale(), $fromEmail);
504
    }
505
506
    /**
507
     * Return the Swift_Mailer to be used for sending mails immediately (e.g. instead of spooling them) if it is configured.
508
     *
509
     * @param $params
510
     *
511 6
     * @return \Swift_Mailer
512
     */
513
    private function getMailer($params)
514 6
    {
515
        // if the second mailer for immediate mail-delivery has been configured
516
        if (null !== $this->immediateMailer) {
517
            // check if this template has been configured to be sent immediately
518
            if (array_key_exists(AzineTemplateProvider::SEND_IMMEDIATELY_FLAG, $params) && $params[AzineTemplateProvider::SEND_IMMEDIATELY_FLAG]) {
519
                return $this->immediateMailer;
520
            }
521 6
        }
522
523
        return $this->mailer;
524
    }
525
526
    /**
527
     * Send confirmation link to specified new user email.
528
     *
529
     * @param UserInterface $user
530
     * @param $confirmationUrl
531
     * @param $toEmail
532
     *
533
     * @return bool
534
     */
535
    public function sendUpdateEmailConfirmation(UserInterface $user, $confirmationUrl, $toEmail)
536
    {
537
        $template = $this->parameters['template']['email_updating'];
538
        $fromEmail = $this->parameters['from_email']['confirmation'];
539
        $context = array(
540
            'user' => $user,
541
            'confirmationUrl' => $confirmationUrl,
542
        );
543
544
        $this->sendMessage($template, $context, $fromEmail, $toEmail);
545
    }
546
}
547