Completed
Push — master ( 227ddc...d26fca )
by Dominik
03:42
created

AzineTwigSwiftMailer::loadTemplate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2
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 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 ManagerRegistry
37
     */
38
    protected $managerRegistry;
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
                                    ManagerRegistry $managerRegistry,
90
                                    EmailOpenTrackingCodeBuilderInterface $emailOpenTrackingCodeBuilder,
91
                                    AzineEmailTwigExtension $emailTwigExtension,
92
                                    array $parameters,
93
                                    \Swift_Mailer $immediateMailer = null)
94
    {
95 7
        parent::__construct($mailer, $router, $twig, $parameters);
96 7
        $this->immediateMailer = $immediateMailer;
97 7
        $this->logger = $logger;
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;
0 ignored issues
show
Bug introduced by
The property emailTwigExtension does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

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