Issues (68)

src/Email/BetterEmail.php (18 issues)

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
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->render();
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
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
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
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->render(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->render();
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);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\View\SSViewer::fromString() has been deprecated: 5.4.0 Will be replaced with SilverStripe\TemplateEngine\SSTemplateEngine::renderString() ( Ignorable by Annotation )

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

510
        $viewer = /** @scrutinizer ignore-deprecated */ SSViewer::fromString($content);

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...
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
    public function render(bool $plainOnly = false): void
525
    {
526
        // Respect explicitly set body
527
        $htmlBody = $plainBody = null;
528
529
        // Only respect if we don't have an email template
530
        if ($this->emailTemplate) {
531
            $htmlBody = $plainOnly ? null : $this->getHtmlBody();
532
            $plainBody = $plainOnly ? $this->getTextBody() : null;
533
        }
534
535
        // Ensure we can at least render something
536
        $htmlTemplate = $this->getHTMLTemplate();
537
        $plainTemplate = $this->getPlainTemplate();
538
        if (!$htmlTemplate && !$plainTemplate && !$plainBody && !$htmlBody) {
539
            return;
540
        }
541
542
        $htmlRender = null;
543
        $plainRender = null;
544
545
        if ($htmlBody && !$this->dataHasBeenSet) {
546
            $htmlRender = $htmlBody;
547
        }
548
549
        if ($plainBody && !$this->dataHasBeenSet) {
550
            $plainRender = $plainBody;
551
        }
552
553
        // Do not interfere with emails styles
554
        Requirements::clear();
555
556
        // Render plain
557
        if (!$plainRender && $plainTemplate) {
558
            $plainRender = $this->getData()->renderWith($plainTemplate)->Plain();
559
            // Do another round of rendering to render our variables inside
560
            $plainRender = $this->renderWithData($plainRender);
561
        }
562
563
        // Render HTML part, either if sending html email, or a plain part is lacking
564
        if (!$htmlRender && $htmlTemplate && (!$plainOnly || empty($plainRender))) {
565
            $htmlRender = $this->getData()->renderWith($htmlTemplate)->RAW();
566
            // Do another round of rendering to render our variables inside
567
            $htmlRender = $this->renderWithData($htmlRender);
568
        }
569
570
        // Render subject with data as well
571
        $subject = $this->renderWithData($this->getSubject());
572
        // Html entities in email titles is not a good idea
573
        $subject = html_entity_decode($subject, ENT_QUOTES | ENT_XML1, 'UTF-8');
574
        // Avoid crazy template name in email
575
        $subject = preg_replace("/<!--(.)+-->/", "", $subject);
576
        parent::setSubject($subject);
577
578
        // Plain render fallbacks to using the html render with html tags removed
579
        if (!$plainRender && $htmlRender) {
580
            $plainRender = EmailUtils::convert_html_to_text($htmlRender);
0 ignored issues
show
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

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

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