Passed
Pull Request — master (#19)
by Sergey
02:21
created

BetterEmail::getData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 3
eloc 9
c 1
b 1
f 0
nc 3
nop 0
dl 0
loc 15
rs 9.9666
1
<?php
2
3
namespace LeKoala\EmailTemplates\Email;
4
5
use Exception;
6
use BadMethodCallException;
7
use LeKoala\EmailTemplates\Extensions\EmailTemplateSiteConfigExtension;
8
use SilverStripe\i18n\i18n;
9
use SilverStripe\Control\HTTP;
10
use SilverStripe\View\SSViewer;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Control\Director;
14
use SilverStripe\View\Requirements;
15
use SilverStripe\Control\Email\Email;
16
use SilverStripe\SiteConfig\SiteConfig;
17
use LeKoala\EmailTemplates\Models\SentEmail;
18
use LeKoala\EmailTemplates\Helpers\EmailUtils;
19
use LeKoala\EmailTemplates\Models\EmailTemplate;
0 ignored issues
show
Bug introduced by
The type LeKoala\EmailTemplates\Models\EmailTemplate 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...
20
use LeKoala\EmailTemplates\Helpers\SubsiteHelper;
21
use SilverStripe\Core\Injector\Injector;
22
use SilverStripe\Security\DefaultAdminService;
23
use SilverStripe\View\ArrayData;
24
use SilverStripe\View\Parsers\ShortcodeParser;
25
use SilverStripe\View\ViewableData;
26
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
27
use Symfony\Component\Mailer\MailerInterface;
28
use Symfony\Component\Mime\Part\AbstractPart;
29
30
/**
31
 * An improved and more pleasant base Email class to use on your project
32
 *
33
 * This class is fully decoupled from the EmailTemplate class and keep be used
34
 * independantly
35
 *
36
 * Improvements are:
37
 *
38
 * - URL safe rewriting
39
 * - Configurable base template (base system use Email class with setHTMLTemplate to provide content)
40
 * - Send email according to member locale
41
 * - Check for subject
42
 * - Send to member or admin
43
 * - Persist emails
44
 * - Parse body (multi part body is supported)
45
 * - Plaintext takes template into account
46
 * - Disable emails
47
 * - Unified send methods that support hooks
48
 *
49
 * @author lekoala
50
 */
51
class BetterEmail extends Email
52
{
53
    const STATE_CANCELLED = 'cancelled';
54
    const STATE_NOT_SENT = 'not_sent';
55
    const STATE_SENT = 'sent';
56
    const STATE_FAILED = 'failed';
57
58
    /**
59
     * @var EmailTemplate|null
60
     */
61
    protected $emailTemplate;
62
63
    /**
64
     * @var string
65
     */
66
    protected $locale;
67
68
    /**
69
     * @var string
70
     */
71
    protected $to;
72
73
    /**
74
     * @var Member|null
75
     */
76
    protected $to_member;
77
78
    /**
79
     * @var string
80
     */
81
    protected $from;
82
83
    /**
84
     * @var Member|null
85
     */
86
    protected $from_member;
87
88
    /**
89
     * @var boolean
90
     */
91
    protected $disabled = false;
92
93
    /**
94
     * @var SentEmail|null
95
     */
96
    protected $sentMail = null;
97
98
    /**
99
     * @var boolean
100
     */
101
    protected $sendingCancelled = false;
102
103
    /**
104
     * Additional data available in a template.
105
     * Used in the same way than {@link ViewableData->customize()}.
106
     */
107
    private ViewableData $data;
108
109
    private bool $dataHasBeenSet = false;
110
111
    /**
112
     * Email constructor.
113
     * @param string|array $from
114
     * @param string|array $to
115
     * @param string $subject
116
     * @param string $body
117
     * @param string|array $cc
118
     * @param string|array $bcc
119
     * @param string $returnPath
120
     */
121
    public function __construct(
122
        string|array $from = '',
123
        string|array $to = '',
124
        string $subject = '',
125
        string $body = '',
126
        string|array $cc = '',
127
        string|array $bcc = '',
128
        string $returnPath = ''
129
    ) {
130
        parent::__construct($from, $to, $subject, $body, $cc, $bcc, $returnPath);
131
132
        // Use template as a layout
133
        if ($defaultTemplate = self::config()->get('template')) {
134
            // Call method because variable is private
135
            parent::setHTMLTemplate($defaultTemplate);
136
        }
137
        $this->data = ViewableData::create();
138
    }
139
140
    /**
141
     * Persists the email to the database
142
     *
143
     * @param bool|array|string $results
144
     * @return SentEmail
145
     */
146
    protected function persist($results)
147
    {
148
        $record = SentEmail::create([
149
            'To' => EmailUtils::format_email_addresses($this->getTo()),
150
            'From' => EmailUtils::format_email_addresses($this->getFrom()),
151
            'ReplyTo' => EmailUtils::format_email_addresses($this->getReplyTo()),
152
            'Subject' => $this->getSubject(),
153
            'Body' => $this->getRenderedBody(),
154
            'Headers' => $this->getHeaders()->toString(),
155
            'CC' => EmailUtils::format_email_addresses($this->getCC()),
156
            'BCC' => EmailUtils::format_email_addresses($this->getBCC()),
157
            'Results' => json_encode($results),
158
        ]);
159
        $record->write();
160
161
        // TODO: migrate this to a cron task
162
        SentEmail::cleanup();
163
164
        return $record;
165
    }
166
167
168
    /**
169
     * Get body of message after rendering
170
     * Useful for previews
171
     *
172
     * @return string
173
     */
174
    public function getRenderedBody()
175
    {
176
        $this->render();
177
        return $this->getHtmlBody();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getHtmlBody() also could return the type resource which is incompatible with the documented return type string.
Loading history...
178
    }
179
180
    /**
181
     * Don't forget that setBody will erase content of html template
182
     * Prefer to use this instead. Basically you can replace setBody calls with this method
183
     * URLs are rewritten by render process
184
     *
185
     * @param string $body
186
     * @return static
187
     */
188
    public function addBody($body)
189
    {
190
        return $this->addData("EmailContent", $body);
191
    }
192
193
    /**
194
     * @param string $body The email body
195
     * @return static
196
     */
197
    public function setBody(AbstractPart|string $body = null): static
198
    {
199
        $this->text(null);
200
201
        $body = self::rewriteURLs($body);
202
        parent::setBody($body);
203
204
        return $this;
205
    }
206
207
    /**
208
     * Get data which is exposed to the template
209
     *
210
     * The following data is exposed via this method by default:
211
     * IsEmail: used to detect if rendering an email template rather than a page template
212
     * BaseUrl: used to get the base URL for the email
213
     */
214
    public function getData(): ViewableData
215
    {
216
        $extraData = [
217
            'IsEmail' => true,
218
            'BaseURL' => Director::absoluteBaseURL(),
219
        ];
220
        $data = clone $this->data;
221
        foreach ($extraData as $key => $value) {
222
            // @phpstan-ignore-next-line
223
            if (is_null($data->{$key})) {
224
                $data->{$key} = $value;
225
            }
226
        }
227
        $this->extend('updateGetData', $data);
228
        return $data;
229
    }
230
231
    /**
232
     * Add data to be used in the template
233
     *
234
     * Calling addData() once means that any content set via text()/html()/setBody() will have no effect
235
     *
236
     * @param string|array $nameOrData can be either the name to add, or an array of [name => value]
237
     */
238
    public function addData(string|array $nameOrData, mixed $value = null): static
239
    {
240
        if (is_array($nameOrData)) {
0 ignored issues
show
introduced by
The condition is_array($nameOrData) is always true.
Loading history...
241
            foreach ($nameOrData as $key => $val) {
242
                $this->data->{$key} = $val;
243
            }
244
        } else {
245
            $this->data->{$nameOrData} = $value;
246
        }
247
        $this->dataHasBeenSet = true;
248
        return $this;
249
    }
250
251
    /**
252
     * Remove a single piece of template data
253
     */
254
    public function removeData(string $name)
255
    {
256
        $this->data->{$name} = null;
257
        return $this;
258
    }
259
260
    /**
261
     * @param array|ViewableData $data The template data to set
262
     * @return $this
263
     */
264
    public function setData($data)
265
    {
266
        // Merge data!
267
        if ($this->emailTemplate) {
268
            if (is_array($data)) {
269
                $this->setDataInternal($data);
270
            } elseif ($data instanceof DataObject) {
271
                $this->setDataInternal($data->toMap());
272
            } else {
273
                $this->setDataInternal($data);
274
            }
275
        } else {
276
            $this->setDataInternal($data);
277
        }
278
        return $this;
279
    }
280
281
    /**
282
     * Set template data
283
     *
284
     * Calling setData() once means that any content set via text()/html()/setBody() will have no effect
285
     */
286
    protected function setDataInternal(array|ViewableData $data)
287
    {
288
        if (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
289
            $data = ArrayData::create($data);
290
        }
291
        $this->data = $data;
292
        $this->dataHasBeenSet = true;
293
        return $this;
294
    }
295
296
    /**
297
     * Sends a HTML email
298
     *
299
     * @return void
300
     */
301
    public function send(): void
302
    {
303
        $this->doSend(false);
304
    }
305
306
    /**
307
     * Sends a plain text email
308
     *
309
     * @return void
310
     */
311
    public function sendPlain(): void
312
    {
313
        $this->doSend(true);
314
    }
315
316
    /**
317
     * Send this email
318
     *
319
     * @param bool $plain
320
     * @return bool|string true if successful or error string on failure
321
     * @throws Exception
322
     */
323
    public function doSend($plain = false)
324
    {
325
        if ($this->disabled) {
326
            $this->sendingCancelled = true;
327
            return false;
328
        }
329
330
        // Check for Subject
331
        if (!$this->getSubject()) {
332
            throw new BadMethodCallException('You must set a subject');
333
        }
334
335
        // This hook can prevent email from being sent
336
        $result = $this->extend('onBeforeDoSend', $this);
337
        if ($result === false) {
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
338
            $this->sendingCancelled = true;
339
            return false;
340
        }
341
342
        $SiteConfig = $this->currentSiteConfig();
343
344
        // Check for Sender and use default if necessary
345
        $from = $this->getFrom();
346
        if (empty($from)) {
347
            $this->setFrom($SiteConfig->EmailDefaultSender());
348
        }
349
350
        // Check for Recipient and use default if necessary
351
        $to = $this->getTo();
352
        if (empty($to)) {
353
            $this->addTo($SiteConfig->EmailDefaultRecipient());
354
        }
355
356
        // Set language to use for the email
357
        $restore_locale = null;
358
        if ($this->locale) {
359
            $restore_locale = i18n::get_locale();
360
            i18n::set_locale($this->locale);
361
        }
362
363
        $member = $this->to_member;
364
        if ($member) {
365
            // Maybe this member doesn't want to receive emails?
366
            // @phpstan-ignore-next-line
367
            if ($member->hasMethod('canReceiveEmails') && !$member->canReceiveEmails()) {
368
                return false;
369
            }
370
        }
371
372
        // Make sure we have a full render with current locale
373
        if ($this->emailTemplate) {
374
            $this->clearBody();
375
        }
376
377
        try {
378
            $res = true;
379
            if ($plain) {
380
                $this->internalSendPlain();
381
            } else {
382
                $this->internalSend();
383
            }
384
        } catch (TransportExceptionInterface $th) {
385
            $res = $th->getMessage();
386
        }
387
388
        if ($restore_locale) {
389
            i18n::set_locale($restore_locale);
390
        }
391
392
        $this->extend('onAfterDoSend', $this, $res);
393
        $this->sentMail = $this->persist($res);
394
395
        return $res;
396
    }
397
398
    private function internalSendPlain()
399
    {
400
        $html = $this->getHtmlBody();
401
        $this->render(true);
402
        $this->html(null);
403
        Injector::inst()->get(MailerInterface::class)->send($this);
404
        $this->html($html);
405
    }
406
407
    private function internalSend()
408
    {
409
        $this->render();
410
        Injector::inst()->get(MailerInterface::class)->send($this);
411
    }
412
413
    /**
414
     * Returns one of the STATE_xxxx constant
415
     *
416
     * @return string
417
     */
418
    public function getSendStatus()
419
    {
420
        if ($this->sendingCancelled) {
421
            return self::STATE_CANCELLED;
422
        }
423
        if ($this->sentMail) {
424
            if ($this->sentMail->IsSuccess()) {
425
                return self::STATE_SENT;
426
            }
427
            return self::STATE_FAILED;
428
        }
429
        return self::STATE_NOT_SENT;
430
    }
431
432
    /**
433
     * Was sending cancelled ?
434
     *
435
     * @return bool
436
     */
437
    public function getSendingCancelled()
438
    {
439
        return $this->sendingCancelled;
440
    }
441
442
    /**
443
     * The last result from "send" method. Null if not sent yet or sending was cancelled
444
     *
445
     * @return SentEmail
446
     */
447
    public function getSentMail()
448
    {
449
        return $this->sentMail;
450
    }
451
452
    /**
453
     * @return $this
454
     */
455
    public function clearBody()
456
    {
457
        $this->setBody(null);
458
        return $this;
459
    }
460
461
    /**
462
     * Set the template to render the email with
463
     *
464
     * This method is overidden in order to look for email templates to provide
465
     * content to
466
     *
467
     * @param string $template
468
     * @return static
469
     */
470
    public function setHTMLTemplate(string $template): static
471
    {
472
        if (substr($template, -3) == '.ss') {
473
            $template = substr($template, 0, -3);
474
        }
475
476
        // Do we have a custom template matching this code?
477
        $code = self::makeTemplateCode($template);
478
        $emailTemplate = EmailTemplate::getByCode($code, false);
479
        if ($emailTemplate) {
480
            $emailTemplate->applyTemplate($this);
481
            return $this;
482
        }
483
484
        // If not, keep default behaviour (call method because var is private)
485
        return parent::setHTMLTemplate($template);
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::setHTMLTemplate($template) returns the type SilverStripe\Control\Email\Email which includes types incompatible with the type-hinted return LeKoala\EmailTemplates\Email\BetterEmail.
Loading history...
486
    }
487
488
    /**
489
     * Make a template code
490
     *
491
     * @param string $str
492
     * @return string
493
     */
494
    public static function makeTemplateCode($str)
495
    {
496
        // If we get a class name
497
        $parts = explode('\\', $str);
498
        $str = end($parts);
499
        $code = preg_replace('/Email$/', '', $str);
500
        return $code;
501
    }
502
503
    /**
504
     * Helper method to render string with data
505
     *
506
     * @param string $content
507
     * @return string
508
     */
509
    public function renderWithData($content)
510
    {
511
        $scParsedContent = htmlspecialchars_decode(trim($content));
512
        $scParsedContent = ShortcodeParser::get()->parse($scParsedContent);
513
        $scParsedContent = str_replace(['{% ', ' %}'], ['<% ', ' %>'], $scParsedContent);
514
515
        $viewer = SSViewer::fromString($scParsedContent);
516
        $data = $this->getData();
517
518
        $result = (string) $viewer->process($data);
519
        $result = self::rewriteURLs($result);
520
        return $result;
521
    }
522
523
    /**
524
     * Call html() and/or text() after rendering email templates
525
     * If either body html or text were previously explicitly set, those values will not be overwritten
526
     *
527
     * @param bool $plainOnly - if true then do not call html()
528
     */
529
    public function render(bool $plainOnly = false): void
530
    {
531
        // Respect explicitly set body
532
        $htmlBody = $plainBody = null;
533
534
        // Only respect if we don't have an email template
535
        if ($this->emailTemplate) {
536
            $htmlBody = $plainOnly ? null : $this->getHtmlBody();
537
            $plainBody = $plainOnly ? $this->getTextBody() : null;
538
        }
539
540
        // Ensure we can at least render something
541
        $htmlTemplate = $this->getHTMLTemplate();
542
        $plainTemplate = $this->getPlainTemplate();
543
        if (!$htmlTemplate && !$plainTemplate && !$plainBody && !$htmlBody) {
544
            return;
545
        }
546
547
        $htmlRender = null;
548
        $plainRender = null;
549
550
        if ($htmlBody && !$this->dataHasBeenSet) {
551
            $htmlRender = $htmlBody;
552
        }
553
554
        if ($plainBody && !$this->dataHasBeenSet) {
555
            $plainRender = $plainBody;
556
        }
557
558
        // Do not interfere with emails styles
559
        Requirements::clear();
560
561
        // Render plain
562
        if (!$plainRender && $plainTemplate) {
563
            $plainRender = $this->getData()->renderWith($plainTemplate)->Plain();
564
            // Do another round of rendering to render our variables inside
565
            $plainRender = $this->renderWithData($plainRender);
566
        }
567
568
        // Render HTML part, either if sending html email, or a plain part is lacking
569
        if (!$htmlRender && $htmlTemplate && (!$plainOnly || empty($plainRender))) {
570
            $htmlRender = $this->getData()->renderWith($htmlTemplate)->RAW();
571
            // Do another round of rendering to render our variables inside
572
            $htmlRender = $this->renderWithData($htmlRender);
573
        }
574
575
        // Render subject with data as well
576
        $subject = $this->renderWithData($this->getSubject());
577
        // Html entities in email titles is not a good idea
578
        $subject = html_entity_decode($subject, ENT_QUOTES | ENT_XML1, 'UTF-8');
579
        // Avoid crazy template name in email
580
        $subject = preg_replace("/<!--(.)+-->/", "", $subject);
581
        parent::setSubject($subject);
582
583
        // Plain render fallbacks to using the html render with html tags removed
584
        if (!$plainRender && $htmlRender) {
585
            $plainRender = EmailUtils::convert_html_to_text($htmlRender);
0 ignored issues
show
Bug introduced by
It seems like $htmlRender can also be of type resource; however, parameter $content of LeKoala\EmailTemplates\H...:convert_html_to_text() does only seem to accept Symfony\Component\Mime\Part\TextPart|string, 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

585
            $plainRender = EmailUtils::convert_html_to_text(/** @scrutinizer ignore-type */ $htmlRender);
Loading history...
586
        }
587
588
        // Rendering is finished
589
        Requirements::restore();
590
591
        // Handle edge case where no template was found
592
        if (!$htmlRender && $htmlBody) {
593
            $htmlRender = $htmlBody;
594
        }
595
596
        if (!$plainRender && $plainBody) {
597
            $plainRender = $plainBody;
598
        }
599
600
        if ($plainRender) {
601
            $this->text($plainRender);
602
        }
603
        if ($htmlRender && !$plainOnly) {
604
            $this->html($htmlRender);
605
        }
606
    }
607
608
    /**
609
     * Get locale set before email is sent
610
     *
611
     * @return string
612
     */
613
    public function getLocale()
614
    {
615
        return $this->locale;
616
    }
617
618
    /**
619
     *  Set locale to set before email is sent
620
     *
621
     * @param string $val
622
     */
623
    public function setLocale($val)
624
    {
625
        $this->locale = $val;
626
    }
627
628
    /**
629
     * Is this email disabled ?
630
     *
631
     * @return boolean
632
     */
633
    public function getDisabled()
634
    {
635
        return $this->disabled;
636
    }
637
638
    /**
639
     * Disable this email (sending will have no effect)
640
     *
641
     * @param bool $disabled
642
     * @return $this
643
     */
644
    public function setDisabled($disabled)
645
    {
646
        $this->disabled = (bool) $disabled;
647
        return $this;
648
    }
649
650
    /**
651
     * Get recipient as member
652
     *
653
     * @return Member
654
     */
655
    public function getToMember()
656
    {
657
        if (!$this->to_member && $this->to) {
658
            $email = EmailUtils::get_email_from_rfc_email($this->to);
659
            $member = Member::get()->filter(['Email' => $email])->first();
660
            if ($member) {
661
                $this->setToMember($member);
662
            }
663
        }
664
        return $this->to_member;
665
    }
666
667
    /**
668
     * Set recipient(s) of the email
669
     *
670
     * To send to many, pass an array:
671
     * array('[email protected]' => 'My Name', '[email protected]');
672
     *
673
     * @param string|array $address The message recipient(s) - if sending to multiple, use an array of address => name
674
     * @param string $name The name of the recipient (if one)
675
     * @return static
676
     */
677
    public function setTo(string|array $address, string $name = ''): static
678
    {
679
        // Allow Name <[email protected]>
680
        if (!$name && is_string($address)) {
0 ignored issues
show
introduced by
The condition is_string($address) is always false.
Loading history...
681
            $name = EmailUtils::get_displayname_from_rfc_email($address);
682
            $address = EmailUtils::get_email_from_rfc_email($address);
683
        }
684
        // Make sure this doesn't conflict with to_member property
685
        if ($this->to_member) {
686
            if (is_string($address)) {
0 ignored issues
show
introduced by
The condition is_string($address) is always false.
Loading history...
687
                // We passed an email that doesn't match to member
688
                if ($this->to_member->Email != $address) {
689
                    $this->to_member = null;
690
                }
691
            } else {
692
                $this->to_member = null;
693
            }
694
        }
695
        $this->to = $address;
0 ignored issues
show
Documentation Bug introduced by
It seems like $address of type array is incompatible with the declared type string of property $to.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
696
        return parent::setTo($address, $name);
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::setTo($address, $name) returns the type SilverStripe\Control\Email\Email which includes types incompatible with the type-hinted return LeKoala\EmailTemplates\Email\BetterEmail.
Loading history...
697
    }
698
699
700
701
    /**
702
     * @param string $subject The Subject line for the email
703
     * @return static
704
     */
705
    public function setSubject(string $subject): static
706
    {
707
        // Do not allow changing subject if a template is set
708
        if ($this->emailTemplate && $this->getSubject()) {
709
            return $this;
710
        }
711
        return parent::setSubject($subject);
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::setSubject($subject) returns the type SilverStripe\Control\Email\Email which includes types incompatible with the type-hinted return LeKoala\EmailTemplates\Email\BetterEmail.
Loading history...
712
    }
713
714
    /**
715
     * Send to admin
716
     *
717
     * @return Email
718
     */
719
    public function setToAdmin()
720
    {
721
        $admin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
722
        return $this->setToMember($admin);
723
    }
724
725
    /**
726
     * Wrapper to report proper SiteConfig type
727
     *
728
     * @return SiteConfig|EmailTemplateSiteConfigExtension
729
     */
730
    public function currentSiteConfig()
731
    {
732
        /** @var SiteConfig|EmailTemplateSiteConfigExtension */
733
        return SiteConfig::current_site_config();
734
    }
735
    /**
736
     * Set to
737
     *
738
     * @return Email
739
     */
740
    public function setToContact()
741
    {
742
        $email = $this->currentSiteConfig()->EmailDefaultRecipient();
743
        return $this->setTo($email);
744
    }
745
746
    /**
747
     * Add in bcc admin
748
     *
749
     * @return Email
750
     */
751
    public function bccToAdmin()
752
    {
753
        $admin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
754
        return $this->addBCC($admin->Email);
755
    }
756
757
    /**
758
     * Add in bcc admin
759
     *
760
     * @return Email
761
     */
762
    public function bccToContact()
763
    {
764
        $email = $this->currentSiteConfig()->EmailDefaultRecipient();
765
        return $this->addBCC($email);
766
    }
767
768
    /**
769
     * Set a member as a recipient.
770
     *
771
     * It will also set the $Recipient variable in the template
772
     *
773
     * @param Member $member
774
     * @param string $locale Locale to use, set to false to keep current locale
775
     * @return BetterEmail
776
     */
777
    public function setToMember(Member $member, $locale = null)
778
    {
779
        if ($locale === null) {
780
            $this->locale = $member->Locale;
781
        } else {
782
            $this->locale = $locale;
783
        }
784
        $this->to_member = $member;
785
786
        $this->addData(['Recipient' => $member]);
787
788
        return $this->setTo($member->Email, $member->getTitle());
789
    }
790
791
    /**
792
     * Get sender as member
793
     *
794
     * @return Member
795
     */
796
    public function getFromMember()
797
    {
798
        if (!$this->from_member && $this->from) {
799
            $email = EmailUtils::get_email_from_rfc_email($this->from);
800
            $member = Member::get()->filter(['Email' => $email])->first();
801
            if ($member) {
802
                $this->setFromMember($member);
803
            }
804
        }
805
        return $this->from_member;
806
    }
807
808
    /**
809
     * Set From Member
810
     *
811
     * It will also set the $Sender variable in the template
812
     *
813
     * @param Member $member
814
     * @return BetterEmail
815
     */
816
    public function setFromMember(Member $member)
817
    {
818
        $this->from_member = $member;
819
820
        $this->addData(['Sender' => $member]);
821
822
        return $this->setFrom($member->Email, $member->getTitle());
823
    }
824
825
    /**
826
     * Improved set from that supports Name <[email protected]> notation
827
     *
828
     * @param string|array $address
829
     * @param string $name
830
     * @return static
831
     */
832
    public function setFrom(string|array $address, string $name = ''): static
833
    {
834
        if (!$name && is_string($address)) {
0 ignored issues
show
introduced by
The condition is_string($address) is always false.
Loading history...
835
            $name = EmailUtils::get_displayname_from_rfc_email($address);
836
            $address = EmailUtils::get_email_from_rfc_email($address);
837
        }
838
        $this->from = $address;
0 ignored issues
show
Documentation Bug introduced by
It seems like $address of type array is incompatible with the declared type string of property $from.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
839
        return parent::setFrom($address, $name);
0 ignored issues
show
Bug Best Practice introduced by
The expression return parent::setFrom($address, $name) returns the type SilverStripe\Control\Email\Email which includes types incompatible with the type-hinted return LeKoala\EmailTemplates\Email\BetterEmail.
Loading history...
840
    }
841
842
    /**
843
     * Bug safe absolute url that support subsites
844
     *
845
     * @param string $url
846
     * @param bool $relativeToSiteBase
847
     * @return string
848
     */
849
    protected static function safeAbsoluteURL($url, $relativeToSiteBase = false)
850
    {
851
        if (empty($url)) {
852
            $absUrl = Director::baseURL();
853
        } else {
854
            $firstCharacter = substr($url, 0, 1);
855
856
            // It's a merge tag, don't touch it because we don't know what kind of url it contains
857
            if (in_array($firstCharacter, ['*', '$', '%'])) {
858
                return $url;
859
            }
860
861
            $absUrl = Director::absoluteURL($url, $relativeToSiteBase ? Director::BASE : Director::ROOT);
862
        }
863
864
        // If we use subsite, absolute url may not use the proper url
865
        $absUrl = SubsiteHelper::safeAbsoluteURL($absUrl);
866
867
        return $absUrl;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $absUrl could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
868
    }
869
870
    /**
871
     * Turn all relative URLs in the content to absolute URLs
872
     */
873
    protected static function rewriteURLs(AbstractPart|string $html = null)
874
    {
875
        if ($html instanceof AbstractPart) {
876
            $html = $html->bodyToString();
877
        }
878
        if (isset($_SERVER['REQUEST_URI'])) {
879
            $html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html ?? '');
880
        }
881
        return HTTP::urlRewriter($html, function ($url) {
0 ignored issues
show
Bug introduced by
It seems like $html can also be of type Symfony\Component\Mime\Part\AbstractPart; however, parameter $content of SilverStripe\Control\HTTP::urlRewriter() does only seem to accept string, 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

881
        return HTTP::urlRewriter(/** @scrutinizer ignore-type */ $html, function ($url) {
Loading history...
882
            //no need to rewrite, if uri has a protocol (determined here by existence of reserved URI character ":")
883
            if (preg_match('/^\w+:/', $url)) {
884
                return $url;
885
            }
886
            return self::safeAbsoluteURL($url, true);
887
        });
888
    }
889
890
    /**
891
     * Get the value of emailTemplate
892
     * @return EmailTemplate
893
     */
894
    public function getEmailTemplate()
895
    {
896
        return $this->emailTemplate;
897
    }
898
899
    /**
900
     * Set the value of emailTemplate
901
     *
902
     * @param EmailTemplate $emailTemplate
903
     * @return $this
904
     */
905
    public function setEmailTemplate(EmailTemplate $emailTemplate)
906
    {
907
        $this->emailTemplate = $emailTemplate;
908
        return $this;
909
    }
910
}
911