Passed
Push — master ( c5bbea...40fe89 )
by Thomas
02:50
created

BetterEmail::setLocale()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace LeKoala\EmailTemplates\Email;
4
5
use BadMethodCallException;
6
use Exception;
7
use Swift_MimePart;
8
use LeKoala\EmailTemplates\Helpers\EmailUtils;
9
use SilverStripe\i18n\i18n;
10
use SilverStripe\Control\HTTP;
11
use SilverStripe\View\SSViewer;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Security\Security;
15
use SilverStripe\View\Requirements;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Core\Config\Config;
18
use SilverStripe\Control\Email\Email;
19
use SilverStripe\SiteConfig\SiteConfig;
20
use LeKoala\EmailTemplates\Models\SentEmail;
21
use LeKoala\EmailTemplates\Helpers\SubsiteHelper;
22
use LeKoala\EmailTemplates\Models\EmailTemplate;
23
24
/**
25
 * An improved and more pleasant base Email class to use on your project
26
 *
27
 * This class is fully decoupled from the EmailTemplate class and keep be used
28
 * independantly
29
 *
30
 * Improvements are:
31
 *
32
 * - URL safe rewriting
33
 * - Configurable base template (base system use Email class with setHTMLTemplate to provide content)
34
 * - Send email according to member locale
35
 * - Check for subject
36
 * - Send to member or admin
37
 * - Persist emails
38
 * - Parse body (multi part body is supported)
39
 * - Plaintext takes template into account
40
 * - Disable emails
41
 * - Unified send methods that support hooks
42
 *
43
 * @author lekoala
44
 */
45
class BetterEmail extends Email
46
{
47
    const STATE_CANCELLED = 'cancelled';
48
    const STATE_NOT_SENT = 'not_sent';
49
    const STATE_SENT = 'sent';
50
    const STATE_FAILED = 'failed';
51
52
    /**
53
     * @var EmailTemplate
54
     */
55
    protected $emailTemplate;
56
57
    /**
58
     * @var string
59
     */
60
    protected $locale;
61
62
    /**
63
     * @var Member
64
     */
65
    protected $to_member;
66
67
    /**
68
     * @var Member
69
     */
70
    protected $from_member;
71
72
    /**
73
     * @var boolean
74
     */
75
    protected $disabled = false;
76
77
    /**
78
     * @var SentMail
0 ignored issues
show
Bug introduced by
The type LeKoala\EmailTemplates\Email\SentMail was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
79
     */
80
    protected $sentMail = null;
81
82
    /**
83
     * @var boolean
84
     */
85
    protected $sendingCancelled = false;
86
87
    /**
88
     * Email constructor.
89
     * @param string|array|null $from
90
     * @param string|array|null $to
91
     * @param string|null $subject
92
     * @param string|null $body
93
     * @param string|array|null $cc
94
     * @param string|array|null $bcc
95
     * @param string|null $returnPath
96
     */
97
    public function __construct(
98
        $from = null,
99
        $to = null,
100
        $subject = null,
101
        $body = null,
102
        $cc = null,
103
        $bcc = null,
104
        $returnPath = null
105
    ) {
106
        parent::__construct($from, $to, $subject, $body, $cc, $bcc, $returnPath);
107
108
        // Use template as a layout
109
        if ($defaultTemplate = self::config()->template) {
110
            // Call method because variable is private
111
            parent::setHTMLTemplate($defaultTemplate);
112
        }
113
    }
114
115
    /**
116
     * Persists the email to the database
117
     *
118
     * @param bool|array $results
119
     * @return SentEmail
120
     */
121
    protected function persist($results)
122
    {
123
        $record = SentEmail::create(array(
124
            'To' => EmailUtils::format_email_addresses($this->getTo()),
125
            'From' => EmailUtils::format_email_addresses($this->getFrom()),
126
            'ReplyTo' => $this->getReplyTo(),
127
            'Subject' => $this->getSubject(),
128
            'Body' => $this->getRenderedBody(),
129
            'Headers' => $this->getSwiftMessage()->getHeaders()->toString(),
130
            'CC' => EmailUtils::format_email_addresses($this->getCC()),
131
            'BCC' => EmailUtils::format_email_addresses($this->getBCC()),
132
            'Results' => json_encode($results),
133
        ));
134
        $record->write();
135
136
        // TODO: migrate this to a cron task
137
        SentEmail::cleanup();
138
139
        return $record;
140
    }
141
142
143
    /**
144
     * Get body of message after rendering
145
     * Useful for previews
146
     *
147
     * @return string
148
     */
149
    public function getRenderedBody()
150
    {
151
        $this->render();
152
        return $this->getSwiftMessage()->getBody();
153
    }
154
155
    /**
156
     * Don't forget that setBody will erase content of html template
157
     * Prefer to use this instead. Basically you can replace setBody calls with this method
158
     * URLs are rewritten by render process
159
     *
160
     * @param string $body
161
     * @return $this
162
     */
163
    public function addBody($body)
164
    {
165
        return $this->owner->addData("EmailContent", $body);
0 ignored issues
show
Bug introduced by
The method addData() does not exist on null. ( Ignorable by Annotation )

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

165
        return $this->owner->/** @scrutinizer ignore-call */ addData("EmailContent", $body);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug Best Practice introduced by
The property owner does not exist on LeKoala\EmailTemplates\Email\BetterEmail. Since you implemented __get, consider adding a @property annotation.
Loading history...
166
    }
167
168
    /**
169
     * Sends a HTML email
170
     *
171
     * @return bool true if successful or array of failed recipients
172
     */
173
    public function send()
174
    {
175
        return $this->doSend(false);
176
    }
177
178
    /**
179
     * Sends a plain text email
180
     *
181
     * @return bool true if successful or array of failed recipients
182
     */
183
    public function sendPlain()
184
    {
185
        return $this->doSend(true);
186
    }
187
188
    /**
189
     * Send this email
190
     *
191
     * @param bool $plain
192
     * @return bool true if successful or array of failed recipients
193
     * @throws Exception
194
     */
195
    public function doSend($plain = false)
196
    {
197
        if ($this->disabled) {
198
            $this->sendingCancelled = true;
199
            return false;
200
        }
201
202
        // Check for Subject
203
        if (!$this->getSubject()) {
204
            throw new BadMethodCallException('You must set a subject');
205
        }
206
207
        // This hook can prevent email from being sent
208
        $result = $this->extend('onBeforeDoSend', $this);
209
        if ($result === false) {
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
210
            $this->sendingCancelled = true;
211
            return false;
212
        }
213
214
        $SiteConfig = SiteConfig::current_site_config();
215
216
        // Check for Sender and use default if necessary
217
        $from = $this->getFrom();
218
        if (empty($from)) {
219
            $this->addFrom($SiteConfig->EmailDefaultSender());
220
        }
221
222
        // Check for Recipient and use default if necessary
223
        $to = $this->getTo();
224
        if (empty($to)) {
225
            $this->addTo($SiteConfig->EmailDefaultRecipient());
226
        }
227
228
        // Set language to use for the email
229
        $restore_locale = null;
230
        if ($this->locale) {
231
            $restore_locale = i18n::get_locale();
232
            i18n::set_locale($this->locale);
233
        }
234
235
        $member = $this->to_member;
236
        if ($member) {
237
            // Maybe this member doesn't want to receive emails?
238
            if ($member->hasMethod('canReceiveEmails') && !$member->canReceiveEmails()) {
239
                return false;
240
            }
241
        }
242
243
        // Make sure we have a full render with current locale
244
        if ($this->emailTemplate) {
245
            $this->clearBody();
246
        }
247
248
        if ($plain) {
249
            // sendPlain will trigger our updated generatePlainPartFromBody
250
            $res = parent::sendPlain();
251
        } else {
252
            $res = parent::send();
253
        }
254
255
        if ($restore_locale) {
256
            i18n::set_locale($restore_locale);
257
        }
258
259
        $this->extend('onAfterDoSend', $this, $res);
260
        $this->sentMail = $this->persist($res);
261
262
        return $res;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $res also could return the type array which is incompatible with the documented return type boolean.
Loading history...
263
    }
264
265
    /**
266
     * Returns one of the STATE_xxxx constant
267
     *
268
     * @return string
269
     */
270
    public function getSendStatus()
271
    {
272
        if ($this->sendingCancelled) {
273
            return self::STATE_CANCELLED;
274
        }
275
        if ($this->sentMail) {
276
            if ($this->sentMail->IsSuccess()) {
277
                return self::STATE_SENT;
278
            }
279
            return self::STATE_FAILED;
280
        }
281
        return self::STATE_NOT_SENT;
282
    }
283
284
    /**
285
     * Was sending cancelled ?
286
     *
287
     * @return bool
288
     */
289
    public function getSendingCancelled()
290
    {
291
        return $this->sendingCancelled;
292
    }
293
294
    /**
295
     * The last result from "send" method. Null if not sent yet or sending was cancelled
296
     *
297
     * @return SentMail
298
     */
299
    public function getSentMail()
300
    {
301
        return $this->sentMail;
302
    }
303
304
    /**
305
     * Automatically adds a plain part to the email generated from the current Body
306
     *
307
     * @return $this
308
     */
309
    public function generatePlainPartFromBody()
310
    {
311
        $plainPart = $this->findPlainPart();
312
        if ($plainPart) {
313
            $this->getSwiftMessage()->detach($plainPart);
314
        }
315
        unset($plainPart);
316
317
        $this->getSwiftMessage()->addPart(
318
            EmailUtils::convert_html_to_text($this->getBody()),
319
            'text/plain',
320
            'utf-8'
321
        );
322
323
        return $this;
324
    }
325
326
    /**
327
     * @return $this
328
     */
329
    public function clearBody()
330
    {
331
        $this->getSwiftMessage()->setBody(null);
332
        return $this;
333
    }
334
335
    /**
336
     * Set the template to render the email with
337
     *
338
     * This method is overidden in order to look for email templates to provide
339
     * content to
340
     *
341
     * @param string $template
342
     * @return $this
343
     */
344
    public function setHTMLTemplate($template)
345
    {
346
        if (substr($template, -3) == '.ss') {
347
            $template = substr($template, 0, -3);
348
        }
349
350
        // Do we have a custom template matching this code?
351
        $code = self::makeTemplateCode($template);
352
        $emailTemplate = EmailTemplate::getByCode($code, false);
353
        if ($emailTemplate) {
0 ignored issues
show
introduced by
$emailTemplate is of type LeKoala\EmailTemplates\Models\EmailTemplate, thus it always evaluated to true.
Loading history...
354
            $emailTemplate->applyTemplate($this);
355
            return $this;
356
        }
357
358
        // If not, keep default behaviour (call method because var is private)
359
        return parent::setHTMLTemplate($template);
360
    }
361
362
    /**
363
     * Make a template code
364
     *
365
     * @param string $str
366
     * @return string
367
     */
368
    public static function makeTemplateCode($str)
369
    {
370
        // If we get a class name
371
        $parts = explode('\\', $str);
372
        $str = end($parts);
373
        $code = preg_replace('/Email$/', '', $str);
374
        return $code;
375
    }
376
377
    /**
378
     * Helper method to render string with data
379
     *
380
     * @param string $content
381
     * @return string
382
     */
383
    public function renderWithData($content)
384
    {
385
        $viewer = SSViewer::fromString($content);
386
        $result = (string) $viewer->process($this, $this->getData());
0 ignored issues
show
Bug introduced by
It seems like $this->getData() can also be of type SilverStripe\View\ViewableData; however, parameter $arguments of SilverStripe\View\SSViewer::process() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

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

386
        $result = (string) $viewer->process($this, /** @scrutinizer ignore-type */ $this->getData());
Loading history...
387
        $result = self::rewriteURLs($result);
388
        return $result;
389
    }
390
391
    /**
392
     * Render the email
393
     * @param bool $plainOnly Only render the message as plain text
394
     * @return $this
395
     */
396
    public function render($plainOnly = false)
397
    {
398
        if ($existingPlainPart = $this->findPlainPart()) {
399
            $this->getSwiftMessage()->detach($existingPlainPart);
400
        }
401
        unset($existingPlainPart);
402
403
        // Respect explicitly set body
404
        $htmlPart = $plainPart = null;
405
406
        // Only respect if we don't have an email template
407
        if ($this->emailTemplate) {
408
            $htmlPart = $plainOnly ? null : $this->getBody();
409
            $plainPart = $plainOnly ? $this->getBody() : null;
410
        }
411
412
        // Ensure we can at least render something
413
        $htmlTemplate = $this->getHTMLTemplate();
414
        $plainTemplate = $this->getPlainTemplate();
415
        if (!$htmlTemplate && !$plainTemplate && !$plainPart && !$htmlPart) {
416
            return $this;
417
        }
418
419
        // Do not interfere with emails styles
420
        Requirements::clear();
421
422
        // Render plain part
423
        if ($plainTemplate && !$plainPart) {
424
            $plainPart = $this->renderWith($plainTemplate, $this->getData())->Plain();
0 ignored issues
show
Bug introduced by
It seems like $this->getData() can also be of type SilverStripe\View\ViewableData; however, parameter $customFields of SilverStripe\View\ViewableData::renderWith() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

424
            $plainPart = $this->renderWith($plainTemplate, /** @scrutinizer ignore-type */ $this->getData())->Plain();
Loading history...
425
            // Do another round of rendering to render our variables inside
426
            $plainPart = $this->renderWithData($plainPart);
427
        }
428
429
        // Render HTML part, either if sending html email, or a plain part is lacking
430
        if (!$htmlPart && $htmlTemplate && (!$plainOnly || empty($plainPart))) {
431
            $htmlPart = $this->renderWith($htmlTemplate, $this->getData());
432
            // Do another round of rendering to render our variables inside
433
            $htmlPart = $this->renderWithData($htmlPart);
434
        }
435
436
        // Render subject with data as well
437
        $subject = $this->renderWithData($this->getSubject());
438
        parent::setSubject($subject);
439
440
        // Plain part fails over to generated from html
441
        if (!$plainPart && $htmlPart) {
442
            $plainPart = EmailUtils::convert_html_to_text($htmlPart);
443
        }
444
445
        // Rendering is finished
446
        Requirements::restore();
447
448
        // Fail if no email to send
449
        if (!$plainPart && !$htmlPart) {
450
            return $this;
451
        }
452
453
        // Build HTML / Plain components
454
        if ($htmlPart && !$plainOnly) {
455
            $this->setBody($htmlPart);
456
            $this->getSwiftMessage()->setContentType('text/html');
457
            $this->getSwiftMessage()->setCharset('utf-8');
458
            if ($plainPart) {
459
                $this->getSwiftMessage()->addPart($plainPart, 'text/plain', 'utf-8');
460
            }
461
        } else {
462
            if ($plainPart) {
463
                $this->setBody($plainPart);
464
            }
465
            $this->getSwiftMessage()->setContentType('text/plain');
466
            $this->getSwiftMessage()->setCharset('utf-8');
467
        }
468
469
        return $this;
470
    }
471
472
    /**
473
     * Get locale set before email is sent
474
     *
475
     * @return string
476
     */
477
    public function getLocale()
478
    {
479
        return $this->locale;
480
    }
481
482
    /**
483
     *  Set locale to set before email is sent
484
     *
485
     * @param string $val
486
     */
487
    public function setLocale($val)
488
    {
489
        $this->locale = $val;
490
    }
491
492
    /**
493
     * Is this email disabled ?
494
     *
495
     * @return boolean
496
     */
497
    public function getDisabled()
498
    {
499
        return $this->disabled;
500
    }
501
502
    /**
503
     * Disable this email (sending will have no effect)
504
     *
505
     * @param bool $disabled
506
     * @return $this
507
     */
508
    public function setDisabled($disabled)
509
    {
510
        $this->disabled = (bool) $disabled;
511
        return $this;
512
    }
513
514
    /**
515
     * Get recipient as member
516
     *
517
     * @return Member
518
     */
519
    public function getToMember()
520
    {
521
        if (!$this->to_member && $this->to) {
0 ignored issues
show
Bug Best Practice introduced by
The property to does not exist on LeKoala\EmailTemplates\Email\BetterEmail. Since you implemented __get, consider adding a @property annotation.
Loading history...
522
            $email = EmailUtils::get_email_from_rfc_email($this->to);
523
            $member = Member::get()->filter(array('Email' => $email))->first();
524
            if ($member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
525
                $this->setToMember($member);
526
            }
527
        }
528
        return $this->to_member;
529
    }
530
531
    /**
532
     * Set recipient(s) of the email
533
     *
534
     * To send to many, pass an array:
535
     * array('[email protected]' => 'My Name', '[email protected]');
536
     *
537
     * @param string|array $address The message recipient(s) - if sending to multiple, use an array of address => name
538
     * @param string|null $name The name of the recipient (if one)
539
     * @return $this
540
     */
541
    public function setTo($address, $name = null)
542
    {
543
        // Make sure this doesn't conflict with to_member property
544
        if ($this->to_member) {
545
            $this->to_member = null;
546
        }
547
        return parent::setTo($address, $name);
548
    }
549
550
    /**
551
     * @param string $subject The Subject line for the email
552
     * @return $this
553
     */
554
    public function setSubject($subject)
555
    {
556
        // Do not allow changing subject if a template is set
557
        if ($this->emailTemplate && $this->getSubject()) {
558
            return $this;
559
        }
560
        return parent::setSubject($subject);
561
    }
562
563
    /**
564
     * Send to admin
565
     *
566
     * @return Email
567
     */
568
    public function setToAdmin()
569
    {
570
        return $this->setToMember(Security::findAnAdministrator());
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Se...::findAnAdministrator() has been deprecated: 4.0.0:5.0.0 Please use DefaultAdminService::findOrCreateDefaultAdmin() ( Ignorable by Annotation )

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

570
        return $this->setToMember(/** @scrutinizer ignore-deprecated */ Security::findAnAdministrator());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
571
    }
572
573
    /**
574
     * Set a member as a recipient.
575
     *
576
     * It will also set the $Recipient variable in the template
577
     *
578
     * @param Member $member
579
     * @param string $locale Locale to use, set to false to keep current locale
580
     * @return BetterEmail
581
     */
582
    public function setToMember(Member $member, $locale = null)
583
    {
584
        if ($locale === null) {
585
            $this->locale = $member->Locale;
586
        } else {
587
            $this->locale = $locale;
588
        }
589
        $this->to_member = $member;
590
591
        $this->addData(array('Recipient' => $member));
592
593
        return $this->setTo($member->Email, $member->getTitle());
594
    }
595
596
    /**
597
     * Get sender as member
598
     *
599
     * @return Member
600
     */
601
    public function getFromMember()
602
    {
603
        if (!$this->from_member && $this->from) {
0 ignored issues
show
Bug Best Practice introduced by
The property from does not exist on LeKoala\EmailTemplates\Email\BetterEmail. Since you implemented __get, consider adding a @property annotation.
Loading history...
604
            $email = EmailUtils::get_email_from_rfc_email($this->from);
605
            $member = Member::get()->filter(array('Email' => $email))->first();
606
            if ($member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
607
                $this->setFromMember($member);
608
            }
609
        }
610
        return $this->from_member;
611
    }
612
613
    /**
614
     * Set From Member
615
     *
616
     * It will also set the $Sender variable in the template
617
     *
618
     * @param Member $member
619
     * @return BetterEmail
620
     */
621
    public function setFromMember(Member $member)
622
    {
623
        $this->from_member = $member;
624
625
        $this->addData(array('Sender' => $member));
626
627
        return $this->setFrom($member->Email);
628
    }
629
630
    /**
631
     * Bug safe absolute url that support subsites
632
     *
633
     * @param string $url
634
     * @param bool $relativeToSiteBase
635
     * @return string
636
     */
637
    protected static function safeAbsoluteURL($url, $relativeToSiteBase = false)
638
    {
639
        if (empty($url)) {
640
            return Director::baseURL();
641
        }
642
        $absUrl = Director::absoluteURL($url, $relativeToSiteBase);
643
644
        // If we use subsite, absolute url may not use the proper url
645
        if (SubsiteHelper::usesSubsite()) {
646
            $subsite = SubsiteHelper::currentSubsite();
647
            if ($subsite->hasMethod('getPrimarySubsiteDomain')) {
648
                $domain = $subsite->getPrimarySubsiteDomain();
649
                $link = $subsite->domain();
650
                $protocol = $domain->getFullProtocol();
651
            } else {
652
                $protocol = Director::protocol();
653
                $link = $subsite->domain();
654
            }
655
            $absUrl = preg_replace('/\/\/[^\/]+\//', '//' . $link . '/', $absUrl);
656
            $absUrl = preg_replace('/http(s)?:\/\//', $protocol, $absUrl);
657
        }
658
659
        return $absUrl;
660
    }
661
662
    /**
663
     * Turn all relative URLs in the content to absolute URLs
664
     */
665
    protected static function rewriteURLs($html)
666
    {
667
        if (isset($_SERVER['REQUEST_URI'])) {
668
            $html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html);
669
        }
670
        return HTTP::urlRewriter($html, function ($url) {
671
            //no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":")
672
            if (preg_match('/^\w+:/', $url)) {
673
                return $url;
674
            }
675
            return self::safeAbsoluteURL($url, true);
676
        });
677
    }
678
679
    /**
680
     * Get the value of emailTemplate
681
     * @return EmailTemplate
682
     */
683
    public function getEmailTemplate()
684
    {
685
        return $this->emailTemplate;
686
    }
687
688
    /**
689
     * Set the value of emailTemplate
690
     *
691
     * @param EmailTemplate $emailTemplate
692
     * @return $this
693
     */
694
    public function setEmailTemplate(EmailTemplate $emailTemplate)
695
    {
696
        $this->emailTemplate = $emailTemplate;
697
        return $this;
698
    }
699
}
700