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

sendUpdateEmailConfirmation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 0
cts 12
cp 0
rs 9.2
c 0
b 0
f 0
cc 2
eloc 13
nc 2
nop 4
crap 6
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