Test Setup Failed
Push — master ( 210134...c17796 )
by Damian
03:18
created

src/Control/Email/Email.php (21 issues)

1
<?php
2
3
namespace SilverStripe\Control\Email;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Control\HTTP;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Core\Environment;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\ORM\FieldType\DBDatetime;
11
use SilverStripe\ORM\FieldType\DBField;
12
use SilverStripe\ORM\FieldType\DBHTMLText;
13
use SilverStripe\View\Requirements;
14
use SilverStripe\View\SSViewer;
15
use SilverStripe\View\ThemeResourceLoader;
16
use SilverStripe\View\ViewableData;
17
use Swift_Message;
18
use Swift_MimePart;
19
20
/**
21
 * Class to support sending emails.
22
 */
23
class Email extends ViewableData
24
{
25
26
    /**
27
     * @var array
28
     * @config
29
     */
30
    private static $send_all_emails_to = array();
31
32
    /**
33
     * @var array
34
     * @config
35
     */
36
    private static $cc_all_emails_to = array();
37
38
    /**
39
     * @var array
40
     * @config
41
     */
42
    private static $bcc_all_emails_to = array();
43
44
    /**
45
     * @var array
46
     * @config
47
     */
48
    private static $send_all_emails_from = array();
49
50
    /**
51
     * This will be set in the config on a site-by-site basis
52
     *
53
     * @config
54
     * @var string The default administrator email address.
55
     */
56
    private static $admin_email = null;
57
58
    /**
59
     * @var Swift_Message
60
     */
61
    private $swiftMessage;
62
63
    /**
64
     * @var string The name of the HTML template to render the email with (without *.ss extension)
65
     */
66
    private $HTMLTemplate = null;
67
68
    /**
69
     * @var string The name of the plain text template to render the plain part of the email with
70
     */
71
    private $plainTemplate = null;
72
73
    /**
74
     * @var Swift_MimePart
75
     */
76
    private $plainPart;
77
78
    /**
79
     * @var array|ViewableData Additional data available in a template.
80
     * Used in the same way than {@link ViewableData->customize()}.
81
     */
82
    private $data = array();
83
84
    /**
85
     * @var array
86
     */
87
    private $failedRecipients = array();
88
89
    /**
90
     * Checks for RFC822-valid email format.
91
     *
92
     * @param string $address
93
     * @return boolean
94
     *
95
     * @copyright Cal Henderson <[email protected]>
96
     *    This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
97
     *    http://creativecommons.org/licenses/by-sa/2.5/
98
     */
99
    public static function is_valid_address($address)
100
    {
101
        return \Swift_Validate::email($address);
102
    }
103
104
    /**
105
     * Get send_all_emails_to
106
     *
107
     * @return array Keys are addresses, values are names
108
     */
109
    public static function getSendAllEmailsTo()
110
    {
111
        return static::mergeConfiguredEmails('send_all_emails_to', 'SS_SEND_ALL_EMAILS_TO');
112
    }
113
114
    /**
115
     * Get cc_all_emails_to
116
     *
117
     * @return array
118
     */
119
    public static function getCCAllEmailsTo()
120
    {
121
        return static::mergeConfiguredEmails('cc_all_emails_to', 'SS_CC_ALL_EMAILS_TO');
122
    }
123
124
    /**
125
     * Get bcc_all_emails_to
126
     *
127
     * @return array
128
     */
129
    public static function getBCCAllEmailsTo()
130
    {
131
        return static::mergeConfiguredEmails('bcc_all_emails_to', 'SS_BCC_ALL_EMAILS_TO');
132
    }
133
134
    /**
135
     * Get send_all_emails_from
136
     *
137
     * @return array
138
     */
139
    public static function getSendAllEmailsFrom()
140
    {
141
        return static::mergeConfiguredEmails('send_all_emails_from', 'SS_SEND_ALL_EMAILS_FROM');
142
    }
143
144
    /**
145
     * Normalise email list from config merged with env vars
146
     *
147
     * @param string $config Config key
148
     * @param string $env Env variable key
149
     * @return array Array of email addresses
150
     */
151
    protected static function mergeConfiguredEmails($config, $env)
152
    {
153
        // Normalise config list
154
        $normalised = [];
155
        $source = (array)static::config()->get($config);
156
        foreach ($source as $address => $name) {
157
            if ($address && !is_numeric($address)) {
158
                $normalised[$address] = $name;
159
            } elseif ($name) {
160
                $normalised[$name] = null;
161
            }
162
        }
163
        $extra = Environment::getEnv($env);
164
        if ($extra) {
165
            $normalised[$extra] = null;
166
        }
167
        return $normalised;
168
    }
169
170
    /**
171
     * Encode an email-address to protect it from spambots.
172
     * At the moment only simple string substitutions,
173
     * which are not 100% safe from email harvesting.
174
     *
175
     * @param string $email Email-address
176
     * @param string $method Method for obfuscating/encoding the address
177
     *    - 'direction': Reverse the text and then use CSS to put the text direction back to normal
178
     *    - 'visible': Simple string substitution ('@' to '[at]', '.' to '[dot], '-' to [dash])
179
     *    - 'hex': Hexadecimal URL-Encoding - useful for mailto: links
180
     * @return string
181
     */
182
    public static function obfuscate($email, $method = 'visible')
183
    {
184
        switch ($method) {
185
            case 'direction':
186
                Requirements::customCSS('span.codedirection { unicode-bidi: bidi-override; direction: rtl; }', 'codedirectionCSS');
187
188
                return '<span class="codedirection">' . strrev($email) . '</span>';
189
            case 'visible':
190
                $obfuscated = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
191
192
                return strtr($email, $obfuscated);
0 ignored issues
show
$obfuscated of type array<string,string> is incompatible with the type string expected by parameter $from of strtr(). ( Ignorable by Annotation )

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

192
                return strtr($email, /** @scrutinizer ignore-type */ $obfuscated);
Loading history...
The call to strtr() has too few arguments starting with to. ( Ignorable by Annotation )

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

192
                return /** @scrutinizer ignore-call */ strtr($email, $obfuscated);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
193
            case 'hex':
194
                $encoded = '';
195
                for ($x = 0; $x < strlen($email); $x++) {
196
                    $encoded .= '&#x' . bin2hex($email{$x}) . ';';
197
                }
198
199
                return $encoded;
200
            default:
201
                user_error('Email::obfuscate(): Unknown obfuscation method', E_USER_NOTICE);
202
203
                return $email;
204
        }
205
    }
206
207
    /**
208
     * Email constructor.
209
     * @param string|array|null $from
210
     * @param string|array|null $to
211
     * @param string|null $subject
212
     * @param string|null $body
213
     * @param string|array|null $cc
214
     * @param string|array|null $bcc
215
     * @param string|null $returnPath
216
     */
217
    public function __construct(
218
        $from = null,
219
        $to = null,
220
        $subject = null,
221
        $body = null,
222
        $cc = null,
223
        $bcc = null,
224
        $returnPath = null
225
    ) {
226
        if ($from) {
227
            $this->setFrom($from);
228
        }
229
        if ($to) {
230
            $this->setTo($to);
231
        }
232
        if ($subject) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subject of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
233
            $this->setSubject($subject);
234
        }
235
        if ($body) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $body of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
236
            $this->setBody($body);
237
        }
238
        if ($cc) {
239
            $this->setCC($cc);
240
        }
241
        if ($bcc) {
242
            $this->setBCC($bcc);
243
        }
244
        if ($returnPath) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $returnPath of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
245
            $this->setReturnPath($returnPath);
246
        }
247
248
        parent::__construct();
249
    }
250
251
    /**
252
     * @return Swift_Message
253
     */
254
    public function getSwiftMessage()
255
    {
256
        if (!$this->swiftMessage) {
257
            $this->setSwiftMessage(new Swift_Message(null, null, 'text/html', 'utf-8'));
258
        }
259
260
        return $this->swiftMessage;
261
    }
262
263
    /**
264
     * @param Swift_Message $swiftMessage
265
     *
266
     * @return $this
267
     */
268
    public function setSwiftMessage($swiftMessage)
269
    {
270
        $swiftMessage->setDate(DBDatetime::now()->getTimestamp());
271
        if (!$swiftMessage->getFrom() && ($defaultFrom = $this->config()->get('admin_email'))) {
272
            $swiftMessage->setFrom($defaultFrom);
273
        }
274
        $this->swiftMessage = $swiftMessage;
275
276
        return $this;
277
    }
278
279
    /**
280
     * @return string[]
281
     */
282
    public function getFrom()
283
    {
284
        return $this->getSwiftMessage()->getFrom();
285
    }
286
287
    /**
288
     * @param string|array $address
289
     * @param string|null $name
290
     * @return $this
291
     */
292
    public function setFrom($address, $name = null)
293
    {
294
        $this->getSwiftMessage()->setFrom($address, $name);
295
296
        return $this;
297
    }
298
299
    /**
300
     * @param string|array $address
301
     * @param string|null $name
302
     * @return $this
303
     */
304
    public function addFrom($address, $name = null)
305
    {
306
        $this->getSwiftMessage()->addFrom($address, $name);
0 ignored issues
show
It seems like $address can also be of type array; however, parameter $address of Swift_Mime_SimpleMessage::addFrom() 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

306
        $this->getSwiftMessage()->addFrom(/** @scrutinizer ignore-type */ $address, $name);
Loading history...
307
308
        return $this;
309
    }
310
311
    /**
312
     * @return string
313
     */
314
    public function getSender()
315
    {
316
        return $this->getSwiftMessage()->getSender();
317
    }
318
319
    /**
320
     * @param string $address
321
     * @param string|null $name
322
     * @return $this
323
     */
324
    public function setSender($address, $name = null)
325
    {
326
        $this->getSwiftMessage()->setSender($address, $name);
327
328
        return $this;
329
    }
330
331
    /**
332
     * @return string
333
     */
334
    public function getReturnPath()
335
    {
336
        return $this->getSwiftMessage()->getReturnPath();
337
    }
338
339
    /**
340
     * The bounce handler address
341
     *
342
     * @param string $address Email address where bounce notifications should be sent
343
     * @return $this
344
     */
345
    public function setReturnPath($address)
346
    {
347
        $this->getSwiftMessage()->setReturnPath($address);
348
        return $this;
349
    }
350
351
    /**
352
     * @return array
353
     */
354
    public function getTo()
355
    {
356
        return $this->getSwiftMessage()->getTo();
357
    }
358
359
    /**
360
     * Set recipient(s) of the email
361
     *
362
     * To send to many, pass an array:
363
     * array('[email protected]' => 'My Name', '[email protected]');
364
     *
365
     * @param string|array $address The message recipient(s) - if sending to multiple, use an array of address => name
366
     * @param string|null $name The name of the recipient (if one)
367
     * @return $this
368
     */
369
    public function setTo($address, $name = null)
370
    {
371
        $this->getSwiftMessage()->setTo($address, $name);
372
373
        return $this;
374
    }
375
376
    /**
377
     * @param string|array $address
378
     * @param string|null $name
379
     * @return $this
380
     */
381
    public function addTo($address, $name = null)
382
    {
383
        $this->getSwiftMessage()->addTo($address, $name);
0 ignored issues
show
It seems like $address can also be of type array; however, parameter $address of Swift_Mime_SimpleMessage::addTo() 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

383
        $this->getSwiftMessage()->addTo(/** @scrutinizer ignore-type */ $address, $name);
Loading history...
384
385
        return $this;
386
    }
387
388
    /**
389
     * @return array
390
     */
391
    public function getCC()
392
    {
393
        return $this->getSwiftMessage()->getCc();
394
    }
395
396
    /**
397
     * @param string|array $address
398
     * @param string|null $name
399
     * @return $this
400
     */
401
    public function setCC($address, $name = null)
402
    {
403
        $this->getSwiftMessage()->setCc($address, $name);
404
405
        return $this;
406
    }
407
408
    /**
409
     * @param string|array $address
410
     * @param string|null $name
411
     * @return $this
412
     */
413
    public function addCC($address, $name = null)
414
    {
415
        $this->getSwiftMessage()->addCc($address, $name);
0 ignored issues
show
It seems like $address can also be of type array; however, parameter $address of Swift_Mime_SimpleMessage::addCc() 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

415
        $this->getSwiftMessage()->addCc(/** @scrutinizer ignore-type */ $address, $name);
Loading history...
416
417
        return $this;
418
    }
419
420
    /**
421
     * @return array
422
     */
423
    public function getBCC()
424
    {
425
        return $this->getSwiftMessage()->getBcc();
426
    }
427
428
    /**
429
     * @param string|array $address
430
     * @param string|null $name
431
     * @return $this
432
     */
433
    public function setBCC($address, $name = null)
434
    {
435
        $this->getSwiftMessage()->setBcc($address, $name);
436
437
        return $this;
438
    }
439
440
    /**
441
     * @param string|array $address
442
     * @param string|null $name
443
     * @return $this
444
     */
445
    public function addBCC($address, $name = null)
446
    {
447
        $this->getSwiftMessage()->addBcc($address, $name);
0 ignored issues
show
It seems like $address can also be of type array; however, parameter $address of Swift_Mime_SimpleMessage::addBcc() 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

447
        $this->getSwiftMessage()->addBcc(/** @scrutinizer ignore-type */ $address, $name);
Loading history...
448
449
        return $this;
450
    }
451
452
    public function getReplyTo()
453
    {
454
        return $this->getSwiftMessage()->getReplyTo();
455
    }
456
457
    /**
458
     * @param string|array $address
459
     * @param string|null $name
460
     * @return $this
461
     */
462
    public function setReplyTo($address, $name = null)
463
    {
464
        $this->getSwiftMessage()->setReplyTo($address, $name);
465
466
        return $this;
467
    }
468
469
    /**
470
     * @param string|array $address
471
     * @param string|null $name
472
     * @return $this
473
     */
474
    public function addReplyTo($address, $name = null)
475
    {
476
        $this->getSwiftMessage()->addReplyTo($address, $name);
0 ignored issues
show
It seems like $address can also be of type array; however, parameter $address of Swift_Mime_SimpleMessage::addReplyTo() 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

476
        $this->getSwiftMessage()->addReplyTo(/** @scrutinizer ignore-type */ $address, $name);
Loading history...
477
478
        return $this;
479
    }
480
481
    /**
482
     * @return string
483
     */
484
    public function getSubject()
485
    {
486
        return $this->getSwiftMessage()->getSubject();
487
    }
488
489
    /**
490
     * @param string $subject The Subject line for the email
491
     * @return $this
492
     */
493
    public function setSubject($subject)
494
    {
495
        $this->getSwiftMessage()->setSubject($subject);
496
497
        return $this;
498
    }
499
500
    /**
501
     * @return int
502
     */
503
    public function getPriority()
504
    {
505
        return $this->getSwiftMessage()->getPriority();
506
    }
507
508
    /**
509
     * @param int $priority
510
     * @return $this
511
     */
512
    public function setPriority($priority)
513
    {
514
        $this->getSwiftMessage()->setPriority($priority);
515
516
        return $this;
517
    }
518
519
    /**
520
     * @param string $path Path to file
521
     * @param string $alias An override for the name of the file
522
     * @param string $mime The mime type for the attachment
523
     * @return $this
524
     */
525
    public function addAttachment($path, $alias = null, $mime = null)
526
    {
527
        $attachment = \Swift_Attachment::fromPath($path);
528
        if ($alias) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $alias of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
529
            $attachment->setFilename($alias);
530
        }
531
        if ($mime) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mime of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
532
            $attachment->setContentType($mime);
533
        }
534
        $this->getSwiftMessage()->attach($attachment);
535
536
        return $this;
537
    }
538
539
    /**
540
     * @param string $data
541
     * @param string $name
542
     * @param string $mime
543
     * @return $this
544
     */
545
    public function addAttachmentFromData($data, $name, $mime = null)
546
    {
547
        $attachment = new \Swift_Attachment($data, $name);
548
        if ($mime) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mime of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
549
            $attachment->setContentType($mime);
550
        }
551
        $this->getSwiftMessage()->attach($attachment);
552
553
        return $this;
554
    }
555
556
    /**
557
     * @return array|ViewableData The template data
558
     */
559
    public function getData()
560
    {
561
        return $this->data;
562
    }
563
564
    /**
565
     * @param array|ViewableData $data The template data to set
566
     * @return $this
567
     */
568
    public function setData($data)
569
    {
570
        $this->data = $data;
571
572
        return $this;
573
    }
574
575
    /**
576
     * @param string|array $name The data name to add or array to names => value
577
     * @param string|null $value The value of the data to add
578
     * @return $this
579
     */
580
    public function addData($name, $value = null)
581
    {
582
        if (is_array($name)) {
583
            $this->data = array_merge($this->data, $name);
0 ignored issues
show
It seems like $this->data can also be of type SilverStripe\View\ViewableData; however, parameter $array1 of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

583
            $this->data = array_merge(/** @scrutinizer ignore-type */ $this->data, $name);
Loading history...
584
        } elseif (is_array($this->data)) {
585
            $this->data[$name] = $value;
586
        } else {
587
            $this->data->$name = $value;
588
        }
589
590
        return $this;
591
    }
592
593
    /**
594
     * Remove a datum from the message
595
     *
596
     * @param string $name
597
     * @return $this
598
     */
599
    public function removeData($name)
600
    {
601
        if (is_array($this->data)) {
602
            unset($this->data[$name]);
603
        } else {
604
            $this->data->$name = null;
605
        }
606
607
        return $this;
608
    }
609
610
    /**
611
     * @return string
612
     */
613
    public function getBody()
614
    {
615
        return $this->getSwiftMessage()->getBody();
616
    }
617
618
    /**
619
     * @param string $body The email body
620
     * @return $this
621
     */
622
    public function setBody($body)
623
    {
624
        $plainPart = $this->findPlainPart();
625
        if ($plainPart) {
626
            $this->getSwiftMessage()->detach($plainPart);
627
        }
628
        unset($plainPart);
629
630
        $body = HTTP::absoluteURLs($body);
631
        $this->getSwiftMessage()->setBody($body);
632
633
        return $this;
634
    }
635
636
    /**
637
     * @return string The base URL for the email
638
     */
639
    public function BaseURL()
640
    {
641
        return Director::absoluteBaseURL();
642
    }
643
644
    /**
645
     * Debugging help
646
     *
647
     * @return string Debug info
648
     */
649
    public function debug()
650
    {
651
        $this->render();
652
653
        $class = static::class;
654
        return "<h2>Email template {$class}:</h2>\n" . '<pre>' . $this->getSwiftMessage()->toString() . '</pre>';
655
    }
656
657
    /**
658
     * @return string
659
     */
660
    public function getHTMLTemplate()
661
    {
662
        if ($this->HTMLTemplate) {
663
            return $this->HTMLTemplate;
664
        }
665
666
        return ThemeResourceLoader::inst()->findTemplate(
667
            SSViewer::get_templates_by_class(static::class, '', self::class),
668
            SSViewer::get_themes()
669
        );
670
    }
671
672
    /**
673
     * Set the template to render the email with
674
     *
675
     * @param string $template
676
     * @return $this
677
     */
678
    public function setHTMLTemplate($template)
679
    {
680
        if (substr($template, -3) == '.ss') {
681
            $template = substr($template, 0, -3);
682
        }
683
        $this->HTMLTemplate = $template;
0 ignored issues
show
Documentation Bug introduced by
It seems like $template can also be of type false. However, the property $HTMLTemplate is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
684
685
        return $this;
686
    }
687
688
    /**
689
     * Get the template to render the plain part with
690
     *
691
     * @return string
692
     */
693
    public function getPlainTemplate()
694
    {
695
        return $this->plainTemplate;
696
    }
697
698
    /**
699
     * Set the template to render the plain part with
700
     *
701
     * @param string $template
702
     * @return $this
703
     */
704
    public function setPlainTemplate($template)
705
    {
706
        if (substr($template, -3) == '.ss') {
707
            $template = substr($template, 0, -3);
708
        }
709
        $this->plainTemplate = $template;
0 ignored issues
show
Documentation Bug introduced by
It seems like $template can also be of type false. However, the property $plainTemplate is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
710
711
        return $this;
712
    }
713
714
    /**
715
     * @param array $recipients
716
     * @return $this
717
     */
718
    public function setFailedRecipients($recipients)
719
    {
720
        $this->failedRecipients = $recipients;
721
722
        return $this;
723
    }
724
725
    /**
726
     * @return array
727
     */
728
    public function getFailedRecipients()
729
    {
730
        return $this->failedRecipients;
731
    }
732
733
    /**
734
     * Used by {@link SSViewer} templates to detect if we're rendering an email template rather than a page template
735
     *
736
     * @return bool
737
     */
738
    public function IsEmail()
739
    {
740
        return true;
741
    }
742
743
    /**
744
     * Send the message to the recipients
745
     *
746
     * @return bool true if successful or array of failed recipients
747
     */
748
    public function send()
749
    {
750
        if (!$this->getBody()) {
751
            $this->render();
752
        }
753
        if (!$this->hasPlainPart()) {
754
            $this->generatePlainPartFromBody();
755
        }
756
        return Injector::inst()->get(Mailer::class)->send($this);
757
    }
758
759
    /**
760
     * @return array|bool
761
     */
762
    public function sendPlain()
763
    {
764
        if (!$this->hasPlainPart()) {
765
            $this->render(true);
766
        }
767
        return Injector::inst()->get(Mailer::class)->send($this);
768
    }
769
770
    /**
771
     * Render the email
772
     * @param bool $plainOnly Only render the message as plain text
773
     * @return $this
774
     */
775
    public function render($plainOnly = false)
776
    {
777
        if ($existingPlainPart = $this->findPlainPart()) {
778
            $this->getSwiftMessage()->detach($existingPlainPart);
779
        }
780
        unset($existingPlainPart);
781
782
        // Respect explicitly set body
783
        $htmlPart = $plainOnly ? null : $this->getBody();
784
        $plainPart = $plainOnly ? $this->getBody() : null;
785
786
        // Ensure we can at least render something
787
        $htmlTemplate = $this->getHTMLTemplate();
788
        $plainTemplate = $this->getPlainTemplate();
789
        if (!$htmlTemplate && !$plainTemplate && !$plainPart && !$htmlPart) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plainPart of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $htmlPart of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
790
            return $this;
791
        }
792
793
        // Render plain part
794
        if ($plainTemplate && !$plainPart) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plainPart of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
795
            $plainPart = $this->renderWith($plainTemplate, $this->getData());
0 ignored issues
show
It seems like $this->getData() can also be of type SilverStripe\View\ViewableData; however, parameter $customFields of SilverStripe\View\ViewableData::renderWith() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

795
            $plainPart = $this->renderWith($plainTemplate, /** @scrutinizer ignore-type */ $this->getData());
Loading history...
796
        }
797
798
        // Render HTML part, either if sending html email, or a plain part is lacking
799
        if (!$htmlPart && $htmlTemplate && (!$plainOnly || empty($plainPart))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $htmlPart of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
800
            $htmlPart = $this->renderWith($htmlTemplate, $this->getData());
801
        }
802
803
        // Plain part fails over to generated from html
804
        if (!$plainPart && $htmlPart) {
805
            /** @var DBHTMLText $htmlPartObject */
806
            $htmlPartObject = DBField::create_field('HTMLFragment', $htmlPart);
807
            $plainPart = $htmlPartObject->Plain();
808
        }
809
810
        // Fail if no email to send
811
        if (!$plainPart && !$htmlPart) {
812
            return $this;
813
        }
814
815
        // Build HTML / Plain components
816
        if ($htmlPart && !$plainOnly) {
817
            $this->setBody($htmlPart);
818
            $this->getSwiftMessage()->setContentType('text/html');
819
            $this->getSwiftMessage()->setCharset('utf-8');
820
            if ($plainPart) {
821
                $this->getSwiftMessage()->addPart($plainPart, 'text/plain', 'utf-8');
822
            }
823
        } else {
824
            if ($plainPart) {
825
                $this->setBody($plainPart);
826
            }
827
            $this->getSwiftMessage()->setContentType('text/plain');
828
            $this->getSwiftMessage()->setCharset('utf-8');
829
        }
830
831
        return $this;
832
    }
833
834
    /**
835
     * @return Swift_MimePart|false
836
     */
837
    public function findPlainPart()
838
    {
839
        foreach ($this->getSwiftMessage()->getChildren() as $child) {
840
            if ($child instanceof Swift_MimePart && $child->getContentType() == 'text/plain') {
841
                return $child;
842
            }
843
        }
844
        return false;
845
    }
846
847
    /**
848
     * @return bool
849
     */
850
    public function hasPlainPart()
851
    {
852
        if ($this->getSwiftMessage()->getContentType() === 'text/plain') {
853
            return true;
854
        }
855
        return (bool) $this->findPlainPart();
856
    }
857
858
    /**
859
     * Automatically adds a plain part to the email generated from the current Body
860
     *
861
     * @return $this
862
     */
863
    public function generatePlainPartFromBody()
864
    {
865
        $plainPart = $this->findPlainPart();
866
        if ($plainPart) {
867
            $this->getSwiftMessage()->detach($plainPart);
868
        }
869
        unset($plainPart);
870
871
        $this->getSwiftMessage()->addPart(
872
            Convert::xml2raw($this->getBody()),
873
            'text/plain',
874
            'utf-8'
875
        );
876
877
        return $this;
878
    }
879
}
880