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

BetterEmail::internalSendPlain()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

584
            $plainRender = EmailUtils::convert_html_to_text(/** @scrutinizer ignore-type */ $htmlRender);
Loading history...
585
        }
586
587
        // Handle edge case where no template was found
588
        if (!$htmlRender && $htmlBody) {
589
            $htmlRender = $htmlBody;
590
        }
591
592
        if (!$plainRender && $plainBody) {
593
            $plainRender = $plainBody;
594
        }
595
596
        if ($plainRender) {
597
            $this->text($plainRender);
598
        }
599
        if ($htmlRender && !$plainOnly) {
600
            $this->html($htmlRender);
601
        }
602
    }
603
604
    /**
605
     * Get locale set before email is sent
606
     *
607
     * @return string
608
     */
609
    public function getLocale()
610
    {
611
        return $this->locale;
612
    }
613
614
    /**
615
     *  Set locale to set before email is sent
616
     *
617
     * @param string $val
618
     */
619
    public function setLocale($val)
620
    {
621
        $this->locale = $val;
622
    }
623
624
    /**
625
     * Is this email disabled ?
626
     *
627
     * @return boolean
628
     */
629
    public function getDisabled()
630
    {
631
        return $this->disabled;
632
    }
633
634
    /**
635
     * Disable this email (sending will have no effect)
636
     *
637
     * @param bool $disabled
638
     * @return $this
639
     */
640
    public function setDisabled($disabled)
641
    {
642
        $this->disabled = (bool) $disabled;
643
        return $this;
644
    }
645
646
    /**
647
     * Get recipient as member
648
     *
649
     * @return Member
650
     */
651
    public function getToMember()
652
    {
653
        if (!$this->to_member && $this->to) {
654
            $email = EmailUtils::get_email_from_rfc_email($this->to);
655
            $member = Member::get()->filter(['Email' => $email])->first();
656
            if ($member) {
657
                $this->setToMember($member);
658
            }
659
        }
660
        return $this->to_member;
661
    }
662
663
    /**
664
     * Set recipient(s) of the email
665
     *
666
     * To send to many, pass an array:
667
     * array('[email protected]' => 'My Name', '[email protected]');
668
     *
669
     * @param string|array $address The message recipient(s) - if sending to multiple, use an array of address => name
670
     * @param string $name The name of the recipient (if one)
671
     * @return static
672
     */
673
    public function setTo(string|array $address, string $name = ''): static
674
    {
675
        // Allow Name <[email protected]>
676
        if (!$name && is_string($address)) {
0 ignored issues
show
introduced by
The condition is_string($address) is always false.
Loading history...
677
            $name = EmailUtils::get_displayname_from_rfc_email($address);
678
            $address = EmailUtils::get_email_from_rfc_email($address);
679
        }
680
        // Make sure this doesn't conflict with to_member property
681
        if ($this->to_member) {
682
            if (is_string($address)) {
0 ignored issues
show
introduced by
The condition is_string($address) is always false.
Loading history...
683
                // We passed an email that doesn't match to member
684
                if ($this->to_member->Email != $address) {
685
                    $this->to_member = null;
686
                }
687
            } else {
688
                $this->to_member = null;
689
            }
690
        }
691
        $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...
692
        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...
693
    }
694
695
696
697
    /**
698
     * @param string $subject The Subject line for the email
699
     * @return static
700
     */
701
    public function setSubject(string $subject): static
702
    {
703
        // Do not allow changing subject if a template is set
704
        if ($this->emailTemplate && $this->getSubject()) {
705
            return $this;
706
        }
707
        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...
708
    }
709
710
    /**
711
     * Send to admin
712
     *
713
     * @return Email
714
     */
715
    public function setToAdmin()
716
    {
717
        $admin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
718
        return $this->setToMember($admin);
719
    }
720
721
    /**
722
     * Wrapper to report proper SiteConfig type
723
     *
724
     * @return SiteConfig|EmailTemplateSiteConfigExtension
725
     */
726
    public function currentSiteConfig()
727
    {
728
        /** @var SiteConfig|EmailTemplateSiteConfigExtension */
729
        return SiteConfig::current_site_config();
730
    }
731
    /**
732
     * Set to
733
     *
734
     * @return Email
735
     */
736
    public function setToContact()
737
    {
738
        $email = $this->currentSiteConfig()->EmailDefaultRecipient();
739
        return $this->setTo($email);
740
    }
741
742
    /**
743
     * Add in bcc admin
744
     *
745
     * @return Email
746
     */
747
    public function bccToAdmin()
748
    {
749
        $admin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
750
        return $this->addBCC($admin->Email);
751
    }
752
753
    /**
754
     * Add in bcc admin
755
     *
756
     * @return Email
757
     */
758
    public function bccToContact()
759
    {
760
        $email = $this->currentSiteConfig()->EmailDefaultRecipient();
761
        return $this->addBCC($email);
762
    }
763
764
    /**
765
     * Set a member as a recipient.
766
     *
767
     * It will also set the $Recipient variable in the template
768
     *
769
     * @param Member $member
770
     * @param string $locale Locale to use, set to false to keep current locale
771
     * @return BetterEmail
772
     */
773
    public function setToMember(Member $member, $locale = null)
774
    {
775
        if ($locale === null) {
776
            $this->locale = $member->Locale;
777
        } else {
778
            $this->locale = $locale;
779
        }
780
        $this->to_member = $member;
781
782
        $this->addData(['Recipient' => $member]);
783
784
        return $this->setTo($member->Email, $member->getTitle());
785
    }
786
787
    /**
788
     * Get sender as member
789
     *
790
     * @return Member
791
     */
792
    public function getFromMember()
793
    {
794
        if (!$this->from_member && $this->from) {
795
            $email = EmailUtils::get_email_from_rfc_email($this->from);
796
            $member = Member::get()->filter(['Email' => $email])->first();
797
            if ($member) {
798
                $this->setFromMember($member);
799
            }
800
        }
801
        return $this->from_member;
802
    }
803
804
    /**
805
     * Set From Member
806
     *
807
     * It will also set the $Sender variable in the template
808
     *
809
     * @param Member $member
810
     * @return BetterEmail
811
     */
812
    public function setFromMember(Member $member)
813
    {
814
        $this->from_member = $member;
815
816
        $this->addData(['Sender' => $member]);
817
818
        return $this->setFrom($member->Email, $member->getTitle());
819
    }
820
821
    /**
822
     * Improved set from that supports Name <[email protected]> notation
823
     *
824
     * @param string|array $address
825
     * @param string $name
826
     * @return static
827
     */
828
    public function setFrom(string|array $address, string $name = ''): static
829
    {
830
        if (!$name && is_string($address)) {
0 ignored issues
show
introduced by
The condition is_string($address) is always false.
Loading history...
831
            $name = EmailUtils::get_displayname_from_rfc_email($address);
832
            $address = EmailUtils::get_email_from_rfc_email($address);
833
        }
834
        $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...
835
        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...
836
    }
837
838
    /**
839
     * Bug safe absolute url that support subsites
840
     *
841
     * @param string $url
842
     * @param bool $relativeToSiteBase
843
     * @return string
844
     */
845
    protected static function safeAbsoluteURL($url, $relativeToSiteBase = false)
846
    {
847
        if (empty($url)) {
848
            $absUrl = Director::baseURL();
849
        } else {
850
            $firstCharacter = substr($url, 0, 1);
851
852
            // It's a merge tag, don't touch it because we don't know what kind of url it contains
853
            if (in_array($firstCharacter, ['*', '$', '%'])) {
854
                return $url;
855
            }
856
857
            $absUrl = Director::absoluteURL($url, $relativeToSiteBase ? Director::BASE : Director::ROOT);
858
        }
859
860
        // If we use subsite, absolute url may not use the proper url
861
        $absUrl = SubsiteHelper::safeAbsoluteURL($absUrl);
862
863
        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...
864
    }
865
866
    /**
867
     * Turn all relative URLs in the content to absolute URLs
868
     */
869
    protected static function rewriteURLs(AbstractPart|string $html = null)
870
    {
871
        if ($html instanceof AbstractPart) {
872
            $html = $html->bodyToString();
873
        }
874
        if (isset($_SERVER['REQUEST_URI'])) {
875
            $html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html ?? '');
876
        }
877
        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

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