Completed
Push — master ( 808aa5...d2904a )
by Michal
17:18
created

Email   F

Complexity

Total Complexity 257

Size/Duplication

Total Lines 1954
Duplicated Lines 5.73 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 257
c 1
b 0
f 0
lcom 1
cbo 11
dl 112
loc 1954
rs 3.9999

64 Methods

Rating   Name   Duplication   Size   Complexity  
A __clone() 0 4 1
A addTo() 0 4 1
A addCc() 0 4 1
A addBcc() 0 4 1
A _validateEmail() 0 11 4
A messageId() 0 15 4
A configuredTransport() 0 4 1
A dropTransport() 0 4 1
A unserialize() 0 4 1
B __construct() 0 27 6
A from() 0 7 2
A sender() 0 7 2
A replyTo() 0 7 2
A readReceipt() 0 7 2
A returnPath() 0 7 2
A to() 7 7 2
A cc() 7 7 2
A bcc() 7 7 2
A charset() 0 11 3
A headerCharset() 0 7 2
A emailPattern() 0 8 2
B _setEmail() 21 21 5
A _setEmailSingle() 0 10 2
B _addEmail() 21 21 5
A subject() 0 8 2
A setHeaders() 0 5 1
A addHeaders() 0 5 1
F getHeaders() 0 71 16
B _formatAddress() 0 16 5
B template() 0 14 5
A viewRender() 0 8 2
A viewVars() 0 8 2
A theme() 0 8 2
A helpers() 0 8 2
A emailFormat() 0 11 3
B transport() 0 22 5
C _constructTransport() 0 36 7
A domain() 0 8 2
C attachments() 0 36 11
A addAttachments() 0 7 1
A message() 0 10 3
D configTransport() 5 27 10
A profile() 0 11 3
C send() 0 25 7
A _logDelivery() 0 21 4
C deliver() 0 23 7
F _applyConfig() 8 50 12
B reset() 0 33 1
A _encode() 0 11 2
A _encodeString() 0 7 2
D _wrap() 0 100 22
A _createBoundary() 0 6 3
C _attachFiles() 0 31 7
A _readFile() 0 5 1
B _attachInlineFiles() 0 24 5
F _render() 36 82 14
A _getTypes() 0 8 2
C _renderTemplates() 0 43 8
A _getContentTransferEncoding() 0 8 2
A _getContentTypeCharset() 0 8 2
B jsonSerialize() 0 27 5
B _checkViewVars() 0 18 7
A createFromArray() 0 13 3
A serialize() 0 10 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Email often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Email, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11
 * @link          http://cakephp.org CakePHP(tm) Project
12
 * @since         2.0.0
13
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Mailer;
16
17
use BadMethodCallException;
18
use Cake\Core\App;
19
use Cake\Core\Configure;
20
use Cake\Core\StaticConfigTrait;
21
use Cake\Filesystem\File;
22
use Cake\Log\Log;
23
use Cake\Network\Http\FormData\Part;
24
use Cake\Utility\Hash;
25
use Cake\Utility\Text;
26
use Cake\View\ViewVarsTrait;
27
use Closure;
28
use Exception;
29
use InvalidArgumentException;
30
use JsonSerializable;
31
use LogicException;
32
use PDO;
33
use RuntimeException;
34
use Serializable;
35
use SimpleXmlElement;
36
37
/**
38
 * CakePHP Email class.
39
 *
40
 * This class is used for sending Internet Message Format based
41
 * on the standard outlined in http://www.rfc-editor.org/rfc/rfc2822.txt
42
 *
43
 * ### Configuration
44
 *
45
 * Configuration for Email is managed by Email::config() and Email::configTransport().
46
 * Email::config() can be used to add or read a configuration profile for Email instances.
47
 * Once made configuration profiles can be used to re-use across various email messages your
48
 * application sends.
49
 */
50
class Email implements JsonSerializable, Serializable
51
{
52
53
    use StaticConfigTrait;
54
    use ViewVarsTrait;
55
56
    /**
57
     * Line length - no should more - RFC 2822 - 2.1.1
58
     *
59
     * @var int
60
     */
61
    const LINE_LENGTH_SHOULD = 78;
62
63
    /**
64
     * Line length - no must more - RFC 2822 - 2.1.1
65
     *
66
     * @var int
67
     */
68
    const LINE_LENGTH_MUST = 998;
69
70
    /**
71
     * Type of message - HTML
72
     *
73
     * @var string
74
     */
75
    const MESSAGE_HTML = 'html';
76
77
    /**
78
     * Type of message - TEXT
79
     *
80
     * @var string
81
     */
82
    const MESSAGE_TEXT = 'text';
83
84
    /**
85
     * Holds the regex pattern for email validation
86
     *
87
     * @var string
88
     */
89
    const EMAIL_PATTERN = '/^((?:[\p{L}0-9.!#$%&\'*+\/=?^_`{|}~-]+)*@[\p{L}0-9-.]+)$/ui';
90
91
    /**
92
     * Recipient of the email
93
     *
94
     * @var array
95
     */
96
    protected $_to = [];
97
98
    /**
99
     * The mail which the email is sent from
100
     *
101
     * @var array
102
     */
103
    protected $_from = [];
104
105
    /**
106
     * The sender email
107
     *
108
     * @var array
109
     */
110
    protected $_sender = [];
111
112
    /**
113
     * The email the recipient will reply to
114
     *
115
     * @var array
116
     */
117
    protected $_replyTo = [];
118
119
    /**
120
     * The read receipt email
121
     *
122
     * @var array
123
     */
124
    protected $_readReceipt = [];
125
126
    /**
127
     * The mail that will be used in case of any errors like
128
     * - Remote mailserver down
129
     * - Remote user has exceeded his quota
130
     * - Unknown user
131
     *
132
     * @var array
133
     */
134
    protected $_returnPath = [];
135
136
    /**
137
     * Carbon Copy
138
     *
139
     * List of email's that should receive a copy of the email.
140
     * The Recipient WILL be able to see this list
141
     *
142
     * @var array
143
     */
144
    protected $_cc = [];
145
146
    /**
147
     * Blind Carbon Copy
148
     *
149
     * List of email's that should receive a copy of the email.
150
     * The Recipient WILL NOT be able to see this list
151
     *
152
     * @var array
153
     */
154
    protected $_bcc = [];
155
156
    /**
157
     * Message ID
158
     *
159
     * @var bool|string
160
     */
161
    protected $_messageId = true;
162
163
    /**
164
     * Domain for messageId generation.
165
     * Needs to be manually set for CLI mailing as env('HTTP_HOST') is empty
166
     *
167
     * @var string
168
     */
169
    protected $_domain = null;
170
171
    /**
172
     * The subject of the email
173
     *
174
     * @var string
175
     */
176
    protected $_subject = '';
177
178
    /**
179
     * Associative array of a user defined headers
180
     * Keys will be prefixed 'X-' as per RFC2822 Section 4.7.5
181
     *
182
     * @var array
183
     */
184
    protected $_headers = [];
185
186
    /**
187
     * Text message
188
     *
189
     * @var string
190
     */
191
    protected $_textMessage = '';
192
193
    /**
194
     * Html message
195
     *
196
     * @var string
197
     */
198
    protected $_htmlMessage = '';
199
200
    /**
201
     * Final message to send
202
     *
203
     * @var array
204
     */
205
    protected $_message = [];
206
207
    /**
208
     * Available formats to be sent.
209
     *
210
     * @var array
211
     */
212
    protected $_emailFormatAvailable = ['text', 'html', 'both'];
213
214
    /**
215
     * What format should the email be sent in
216
     *
217
     * @var string
218
     */
219
    protected $_emailFormat = 'text';
220
221
    /**
222
     * The transport instance to use for sending mail.
223
     *
224
     * @var \Cake\Mailer\AbstractTransport
225
     */
226
    protected $_transport = null;
227
228
    /**
229
     * Charset the email body is sent in
230
     *
231
     * @var string
232
     */
233
    public $charset = 'utf-8';
234
235
    /**
236
     * Charset the email header is sent in
237
     * If null, the $charset property will be used as default
238
     *
239
     * @var string
240
     */
241
    public $headerCharset = null;
242
243
    /**
244
     * The application wide charset, used to encode headers and body
245
     *
246
     * @var string
247
     */
248
    protected $_appCharset = null;
249
250
    /**
251
     * List of files that should be attached to the email.
252
     *
253
     * Only absolute paths
254
     *
255
     * @var array
256
     */
257
    protected $_attachments = [];
258
259
    /**
260
     * If set, boundary to use for multipart mime messages
261
     *
262
     * @var string
263
     */
264
    protected $_boundary = null;
265
266
    /**
267
     * An array mapping url schemes to fully qualified Transport class names
268
     *
269
     * @var array
270
     */
271
    protected static $_dsnClassMap = [
272
        'debug' => 'Cake\Mailer\Transport\DebugTransport',
273
        'mail' => 'Cake\Mailer\Transport\MailTransport',
274
        'smtp' => 'Cake\Mailer\Transport\SmtpTransport',
275
    ];
276
277
    /**
278
     * Configuration profiles for transports.
279
     *
280
     * @var array
281
     */
282
    protected static $_transportConfig = [];
283
284
    /**
285
     * A copy of the configuration profile for this
286
     * instance. This copy can be modified with Email::profile().
287
     *
288
     * @var array
289
     */
290
    protected $_profile = [];
291
292
    /**
293
     * 8Bit character sets
294
     *
295
     * @var array
296
     */
297
    protected $_charset8bit = ['UTF-8', 'SHIFT_JIS'];
298
299
    /**
300
     * Define Content-Type charset name
301
     *
302
     * @var array
303
     */
304
    protected $_contentTypeCharset = [
305
        'ISO-2022-JP-MS' => 'ISO-2022-JP'
306
    ];
307
308
    /**
309
     * Regex for email validation
310
     *
311
     * If null, filter_var() will be used. Use the emailPattern() method
312
     * to set a custom pattern.'
313
     *
314
     * @var string
315
     */
316
    protected $_emailPattern = self::EMAIL_PATTERN;
317
318
    /**
319
     * The class name used for email configuration.
320
     *
321
     * @var string
322
     */
323
    protected $_configClass = 'EmailConfig';
324
325
    /**
326
     * Constructor
327
     *
328
     * @param array|string|null $config Array of configs, or string to load configs from email.php
329
     */
330
    public function __construct($config = null)
331
    {
332
        $this->_appCharset = Configure::read('App.encoding');
333
        if ($this->_appCharset !== null) {
334
            $this->charset = $this->_appCharset;
335
        }
336
        $this->_domain = preg_replace('/\:\d+$/', '', env('HTTP_HOST'));
337
        if (empty($this->_domain)) {
338
            $this->_domain = php_uname('n');
339
        }
340
341
        $this->viewBuilder()
342
            ->className('Cake\View\View')
343
            ->template('')
344
            ->layout('default')
345
            ->helpers(['Html']);
346
347
        if ($config === null) {
348
            $config = static::config('default');
349
        }
350
        if ($config) {
351
            $this->profile($config);
352
        }
353
        if (empty($this->headerCharset)) {
354
            $this->headerCharset = $this->charset;
355
        }
356
    }
357
358
    /**
359
     * Clone ViewBuilder instance when email object is cloned.
360
     *
361
     * @return void
362
     */
363
    public function __clone()
364
    {
365
        $this->_viewBuilder = clone $this->viewBuilder();
366
    }
367
368
    /**
369
     * From
370
     *
371
     * @param string|array|null $email Null to get, String with email,
372
     *   Array with email as key, name as value or email as value (without name)
373
     * @param string|null $name Name
374
     * @return array|$this
375
     * @throws \InvalidArgumentException
376
     */
377
    public function from($email = null, $name = null)
378
    {
379
        if ($email === null) {
380
            return $this->_from;
381
        }
382
        return $this->_setEmailSingle('_from', $email, $name, 'From requires only 1 email address.');
383
    }
384
385
    /**
386
     * Sender
387
     *
388
     * @param string|array|null $email Null to get, String with email,
389
     *   Array with email as key, name as value or email as value (without name)
390
     * @param string|null $name Name
391
     * @return array|$this
392
     * @throws \InvalidArgumentException
393
     */
394
    public function sender($email = null, $name = null)
395
    {
396
        if ($email === null) {
397
            return $this->_sender;
398
        }
399
        return $this->_setEmailSingle('_sender', $email, $name, 'Sender requires only 1 email address.');
400
    }
401
402
    /**
403
     * Reply-To
404
     *
405
     * @param string|array|null $email Null to get, String with email,
406
     *   Array with email as key, name as value or email as value (without name)
407
     * @param string|null $name Name
408
     * @return array|$this
409
     * @throws \InvalidArgumentException
410
     */
411
    public function replyTo($email = null, $name = null)
412
    {
413
        if ($email === null) {
414
            return $this->_replyTo;
415
        }
416
        return $this->_setEmailSingle('_replyTo', $email, $name, 'Reply-To requires only 1 email address.');
417
    }
418
419
    /**
420
     * Read Receipt (Disposition-Notification-To header)
421
     *
422
     * @param string|array|null $email Null to get, String with email,
423
     *   Array with email as key, name as value or email as value (without name)
424
     * @param string|null $name Name
425
     * @return array|$this
426
     * @throws \InvalidArgumentException
427
     */
428
    public function readReceipt($email = null, $name = null)
429
    {
430
        if ($email === null) {
431
            return $this->_readReceipt;
432
        }
433
        return $this->_setEmailSingle('_readReceipt', $email, $name, 'Disposition-Notification-To requires only 1 email address.');
434
    }
435
436
    /**
437
     * Return Path
438
     *
439
     * @param string|array|null $email Null to get, String with email,
440
     *   Array with email as key, name as value or email as value (without name)
441
     * @param string|null $name Name
442
     * @return array|$this
443
     * @throws \InvalidArgumentException
444
     */
445
    public function returnPath($email = null, $name = null)
446
    {
447
        if ($email === null) {
448
            return $this->_returnPath;
449
        }
450
        return $this->_setEmailSingle('_returnPath', $email, $name, 'Return-Path requires only 1 email address.');
451
    }
452
453
    /**
454
     * To
455
     *
456
     * @param string|array|null $email Null to get, String with email,
457
     *   Array with email as key, name as value or email as value (without name)
458
     * @param string|null $name Name
459
     * @return array|$this
460
     */
461 View Code Duplication
    public function to($email = null, $name = null)
462
    {
463
        if ($email === null) {
464
            return $this->_to;
465
        }
466
        return $this->_setEmail('_to', $email, $name);
467
    }
468
469
    /**
470
     * Add To
471
     *
472
     * @param string|array $email Null to get, String with email,
473
     *   Array with email as key, name as value or email as value (without name)
474
     * @param string|null $name Name
475
     * @return $this
476
     */
477
    public function addTo($email, $name = null)
478
    {
479
        return $this->_addEmail('_to', $email, $name);
480
    }
481
482
    /**
483
     * Cc
484
     *
485
     * @param string|array|null $email Null to get, String with email,
486
     *   Array with email as key, name as value or email as value (without name)
487
     * @param string|null $name Name
488
     * @return array|$this
489
     */
490 View Code Duplication
    public function cc($email = null, $name = null)
491
    {
492
        if ($email === null) {
493
            return $this->_cc;
494
        }
495
        return $this->_setEmail('_cc', $email, $name);
496
    }
497
498
    /**
499
     * Add Cc
500
     *
501
     * @param string|array $email Null to get, String with email,
502
     *   Array with email as key, name as value or email as value (without name)
503
     * @param string|null $name Name
504
     * @return $this
505
     */
506
    public function addCc($email, $name = null)
507
    {
508
        return $this->_addEmail('_cc', $email, $name);
509
    }
510
511
    /**
512
     * Bcc
513
     *
514
     * @param string|array|null $email Null to get, String with email,
515
     *   Array with email as key, name as value or email as value (without name)
516
     * @param string|null $name Name
517
     * @return array|$this
518
     */
519 View Code Duplication
    public function bcc($email = null, $name = null)
520
    {
521
        if ($email === null) {
522
            return $this->_bcc;
523
        }
524
        return $this->_setEmail('_bcc', $email, $name);
525
    }
526
527
    /**
528
     * Add Bcc
529
     *
530
     * @param string|array $email Null to get, String with email,
531
     *   Array with email as key, name as value or email as value (without name)
532
     * @param string|null $name Name
533
     * @return $this
534
     */
535
    public function addBcc($email, $name = null)
536
    {
537
        return $this->_addEmail('_bcc', $email, $name);
538
    }
539
540
    /**
541
     * Charset setter/getter
542
     *
543
     * @param string|null $charset Character set.
544
     * @return string this->charset
545
     */
546
    public function charset($charset = null)
547
    {
548
        if ($charset === null) {
549
            return $this->charset;
550
        }
551
        $this->charset = $charset;
552
        if (empty($this->headerCharset)) {
553
            $this->headerCharset = $charset;
554
        }
555
        return $this->charset;
556
    }
557
558
    /**
559
     * HeaderCharset setter/getter
560
     *
561
     * @param string|null $charset Character set.
562
     * @return string this->charset
563
     */
564
    public function headerCharset($charset = null)
565
    {
566
        if ($charset === null) {
567
            return $this->headerCharset;
568
        }
569
        return $this->headerCharset = $charset;
570
    }
571
572
    /**
573
     * EmailPattern setter/getter
574
     *
575
     * @param string|bool|null $regex The pattern to use for email address validation,
576
     *   null to unset the pattern and make use of filter_var() instead, false or
577
     *   nothing to return the current value
578
     * @return string|$this
579
     */
580
    public function emailPattern($regex = false)
581
    {
582
        if ($regex === false) {
583
            return $this->_emailPattern;
584
        }
585
        $this->_emailPattern = $regex;
0 ignored issues
show
Documentation Bug introduced by
It seems like $regex can also be of type boolean. However, the property $_emailPattern 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...
586
        return $this;
587
    }
588
589
    /**
590
     * Set email
591
     *
592
     * @param string $varName Property name
593
     * @param string|array $email String with email,
594
     *   Array with email as key, name as value or email as value (without name)
595
     * @param string $name Name
596
     * @return $this
597
     * @throws \InvalidArgumentException
598
     */
599 View Code Duplication
    protected function _setEmail($varName, $email, $name)
600
    {
601
        if (!is_array($email)) {
602
            $this->_validateEmail($email);
603
            if ($name === null) {
604
                $name = $email;
605
            }
606
            $this->{$varName} = [$email => $name];
607
            return $this;
608
        }
609
        $list = [];
610
        foreach ($email as $key => $value) {
611
            if (is_int($key)) {
612
                $key = $value;
613
            }
614
            $this->_validateEmail($key);
615
            $list[$key] = $value;
616
        }
617
        $this->{$varName} = $list;
618
        return $this;
619
    }
620
621
    /**
622
     * Validate email address
623
     *
624
     * @param string $email Email address to validate
625
     * @return void
626
     * @throws \InvalidArgumentException If email address does not validate
627
     */
628
    protected function _validateEmail($email)
629
    {
630
        if ($this->_emailPattern === null) {
631
            if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
632
                return;
633
            }
634
        } elseif (preg_match($this->_emailPattern, $email)) {
635
            return;
636
        }
637
        throw new InvalidArgumentException(sprintf('Invalid email: "%s"', $email));
638
    }
639
640
    /**
641
     * Set only 1 email
642
     *
643
     * @param string $varName Property name
644
     * @param string|array $email String with email,
645
     *   Array with email as key, name as value or email as value (without name)
646
     * @param string $name Name
647
     * @param string $throwMessage Exception message
648
     * @return $this
649
     * @throws \InvalidArgumentException
650
     */
651
    protected function _setEmailSingle($varName, $email, $name, $throwMessage)
652
    {
653
        $current = $this->{$varName};
654
        $this->_setEmail($varName, $email, $name);
655
        if (count($this->{$varName}) !== 1) {
656
            $this->{$varName} = $current;
657
            throw new InvalidArgumentException($throwMessage);
658
        }
659
        return $this;
660
    }
661
662
    /**
663
     * Add email
664
     *
665
     * @param string $varName Property name
666
     * @param string|array $email String with email,
667
     *   Array with email as key, name as value or email as value (without name)
668
     * @param string $name Name
669
     * @return $this
670
     * @throws \InvalidArgumentException
671
     */
672 View Code Duplication
    protected function _addEmail($varName, $email, $name)
673
    {
674
        if (!is_array($email)) {
675
            $this->_validateEmail($email);
676
            if ($name === null) {
677
                $name = $email;
678
            }
679
            $this->{$varName}[$email] = $name;
680
            return $this;
681
        }
682
        $list = [];
683
        foreach ($email as $key => $value) {
684
            if (is_int($key)) {
685
                $key = $value;
686
            }
687
            $this->_validateEmail($key);
688
            $list[$key] = $value;
689
        }
690
        $this->{$varName} = array_merge($this->{$varName}, $list);
691
        return $this;
692
    }
693
694
    /**
695
     * Get/Set Subject.
696
     *
697
     * @param string|null $subject Subject string.
698
     * @return string|$this
699
     */
700
    public function subject($subject = null)
701
    {
702
        if ($subject === null) {
703
            return $this->_subject;
704
        }
705
        $this->_subject = $this->_encode((string)$subject);
706
        return $this;
707
    }
708
709
    /**
710
     * Sets headers for the message
711
     *
712
     * @param array $headers Associative array containing headers to be set.
713
     * @return $this
714
     */
715
    public function setHeaders(array $headers)
716
    {
717
        $this->_headers = $headers;
718
        return $this;
719
    }
720
721
    /**
722
     * Add header for the message
723
     *
724
     * @param array $headers Headers to set.
725
     * @return $this
726
     */
727
    public function addHeaders(array $headers)
728
    {
729
        $this->_headers = array_merge($this->_headers, $headers);
730
        return $this;
731
    }
732
733
    /**
734
     * Get list of headers
735
     *
736
     * ### Includes:
737
     *
738
     * - `from`
739
     * - `replyTo`
740
     * - `readReceipt`
741
     * - `returnPath`
742
     * - `to`
743
     * - `cc`
744
     * - `bcc`
745
     * - `subject`
746
     *
747
     * @param array $include List of headers.
748
     * @return array
749
     */
750
    public function getHeaders(array $include = [])
751
    {
752
        if ($include == array_values($include)) {
753
            $include = array_fill_keys($include, true);
754
        }
755
        $defaults = array_fill_keys(
756
            [
757
                'from', 'sender', 'replyTo', 'readReceipt', 'returnPath',
758
                'to', 'cc', 'bcc', 'subject'],
759
            false
760
        );
761
        $include += $defaults;
762
763
        $headers = [];
764
        $relation = [
765
            'from' => 'From',
766
            'replyTo' => 'Reply-To',
767
            'readReceipt' => 'Disposition-Notification-To',
768
            'returnPath' => 'Return-Path'
769
        ];
770
        foreach ($relation as $var => $header) {
771
            if ($include[$var]) {
772
                $var = '_' . $var;
773
                $headers[$header] = current($this->_formatAddress($this->{$var}));
774
            }
775
        }
776
        if ($include['sender']) {
777
            if (key($this->_sender) === key($this->_from)) {
778
                $headers['Sender'] = '';
779
            } else {
780
                $headers['Sender'] = current($this->_formatAddress($this->_sender));
781
            }
782
        }
783
784
        foreach (['to', 'cc', 'bcc'] as $var) {
785
            if ($include[$var]) {
786
                $classVar = '_' . $var;
787
                $headers[ucfirst($var)] = implode(', ', $this->_formatAddress($this->{$classVar}));
788
            }
789
        }
790
791
        $headers += $this->_headers;
792
        if (!isset($headers['Date'])) {
793
            $headers['Date'] = date(DATE_RFC2822);
794
        }
795
        if ($this->_messageId !== false) {
796
            if ($this->_messageId === true) {
797
                $headers['Message-ID'] = '<' . str_replace('-', '', Text::uuid()) . '@' . $this->_domain . '>';
798
            } else {
799
                $headers['Message-ID'] = $this->_messageId;
800
            }
801
        }
802
803
        if ($include['subject']) {
804
            $headers['Subject'] = $this->_subject;
805
        }
806
807
        $headers['MIME-Version'] = '1.0';
808
        if (!empty($this->_attachments)) {
809
            $headers['Content-Type'] = 'multipart/mixed; boundary="' . $this->_boundary . '"';
810
        } elseif ($this->_emailFormat === 'both') {
811
            $headers['Content-Type'] = 'multipart/alternative; boundary="' . $this->_boundary . '"';
812
        } elseif ($this->_emailFormat === 'text') {
813
            $headers['Content-Type'] = 'text/plain; charset=' . $this->_getContentTypeCharset();
814
        } elseif ($this->_emailFormat === 'html') {
815
            $headers['Content-Type'] = 'text/html; charset=' . $this->_getContentTypeCharset();
816
        }
817
        $headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding();
818
819
        return $headers;
820
    }
821
822
    /**
823
     * Format addresses
824
     *
825
     * If the address contains non alphanumeric/whitespace characters, it will
826
     * be quoted as characters like `:` and `,` are known to cause issues
827
     * in address header fields.
828
     *
829
     * @param array $address Addresses to format.
830
     * @return array
831
     */
832
    protected function _formatAddress($address)
833
    {
834
        $return = [];
835
        foreach ($address as $email => $alias) {
836
            if ($email === $alias) {
837
                $return[] = $email;
838
            } else {
839
                $encoded = $this->_encode($alias);
840
                if ($encoded === $alias && preg_match('/[^a-z0-9 ]/i', $encoded)) {
841
                    $encoded = '"' . str_replace('"', '\"', $encoded) . '"';
842
                }
843
                $return[] = sprintf('%s <%s>', $encoded, $email);
844
            }
845
        }
846
        return $return;
847
    }
848
849
    /**
850
     * Template and layout
851
     *
852
     * @param bool|string $template Template name or null to not use
853
     * @param bool|string $layout Layout name or null to not use
854
     * @return array|$this
855
     */
856
    public function template($template = false, $layout = false)
857
    {
858
        if ($template === false) {
859
            return [
860
                'template' => $this->viewBuilder()->template(),
861
                'layout' => $this->viewBuilder()->layout()
862
            ];
863
        }
864
        $this->viewBuilder()->template($template ?: '');
0 ignored issues
show
Bug introduced by
It seems like $template ?: '' can also be of type boolean; however, Cake\View\ViewBuilder::template() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
865
        if ($layout !== false) {
866
            $this->viewBuilder()->layout($layout ?: false);
0 ignored issues
show
Bug introduced by
It seems like $layout ?: false can also be of type boolean; however, Cake\View\ViewBuilder::layout() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
867
        }
868
        return $this;
869
    }
870
871
    /**
872
     * View class for render
873
     *
874
     * @param string|null $viewClass View class name.
875
     * @return string|$this
876
     */
877
    public function viewRender($viewClass = null)
878
    {
879
        if ($viewClass === null) {
880
            return $this->viewBuilder()->className();
881
        }
882
        $this->viewBuilder()->className($viewClass);
883
        return $this;
884
    }
885
886
    /**
887
     * Variables to be set on render
888
     *
889
     * @param array|null $viewVars Variables to set for view.
890
     * @return array|$this
891
     */
892
    public function viewVars($viewVars = null)
893
    {
894
        if ($viewVars === null) {
895
            return $this->viewVars;
896
        }
897
        $this->set((array)$viewVars);
898
        return $this;
899
    }
900
901
    /**
902
     * Theme to use when rendering
903
     *
904
     * @param string|null $theme Theme name.
905
     * @return string|$this
906
     */
907
    public function theme($theme = null)
908
    {
909
        if ($theme === null) {
910
            return $this->viewBuilder()->theme();
911
        }
912
        $this->viewBuilder()->theme($theme);
913
        return $this;
914
    }
915
916
    /**
917
     * Helpers to be used in render
918
     *
919
     * @param array|null $helpers Helpers list.
920
     * @return array|$this
921
     */
922
    public function helpers($helpers = null)
923
    {
924
        if ($helpers === null) {
925
            return $this->viewBuilder()->helpers();
926
        }
927
        $this->viewBuilder()->helpers((array)$helpers, false);
928
        return $this;
929
    }
930
931
    /**
932
     * Email format
933
     *
934
     * @param string|null $format Formatting string.
935
     * @return string|$this
936
     * @throws \InvalidArgumentException
937
     */
938
    public function emailFormat($format = null)
939
    {
940
        if ($format === null) {
941
            return $this->_emailFormat;
942
        }
943
        if (!in_array($format, $this->_emailFormatAvailable)) {
944
            throw new InvalidArgumentException('Format not available.');
945
        }
946
        $this->_emailFormat = $format;
947
        return $this;
948
    }
949
950
    /**
951
     * Get/set the transport.
952
     *
953
     * When setting the transport you can either use the name
954
     * of a configured transport or supply a constructed transport.
955
     *
956
     * @param string|AbstractTransport|null $name Either the name of a configured
957
     *   transport, or a transport instance.
958
     * @return \Cake\Mailer\AbstractTransport|$this
959
     * @throws \LogicException When the chosen transport lacks a send method.
960
     * @throws \InvalidArgumentException When $name is neither a string nor an object.
961
     */
962
    public function transport($name = null)
963
    {
964
        if ($name === null) {
965
            return $this->_transport;
966
        }
967
968
        if (is_string($name)) {
969
            $transport = $this->_constructTransport($name);
970
        } elseif (is_object($name)) {
971
            $transport = $name;
972
        } else {
973
            throw new InvalidArgumentException(
974
                sprintf('The value passed for the "$name" argument must be either a string, or an object, %s given.', gettype($name))
975
            );
976
        }
977
        if (!method_exists($transport, 'send')) {
978
            throw new LogicException(sprintf('The "%s" do not have send method.', get_class($transport)));
979
        }
980
981
        $this->_transport = $transport;
982
        return $this;
983
    }
984
985
    /**
986
     * Build a transport instance from configuration data.
987
     *
988
     * @param string $name The transport configuration name to build.
989
     * @return \Cake\Mailer\AbstractTransport
990
     * @throws \InvalidArgumentException When transport configuration is missing or invalid.
991
     */
992
    protected function _constructTransport($name)
993
    {
994
        if (!isset(static::$_transportConfig[$name])) {
995
            throw new InvalidArgumentException(sprintf('Transport config "%s" is missing.', $name));
996
        }
997
998
        if (!isset(static::$_transportConfig[$name]['className'])) {
999
            throw new InvalidArgumentException(
1000
                sprintf('Transport config "%s" is invalid, the required `className` option is missing', $name)
1001
            );
1002
        }
1003
1004
        $config = static::$_transportConfig[$name];
1005
1006
        if (is_object($config['className'])) {
1007
            return $config['className'];
1008
        }
1009
1010
        $className = App::className($config['className'], 'Mailer/Transport', 'Transport');
1011
        if (!$className) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false 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...
1012
            $className = App::className($config['className'], 'Network/Email', 'Transport');
1013
            trigger_error(
1014
                'Transports in "Network/Email" are deprecated, use "Mailer/Transport" instead.',
1015
                E_USER_WARNING
1016
            );
1017
        }
1018
1019
        if (!$className) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false 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...
1020
            throw new InvalidArgumentException(sprintf('Transport class "%s" not found.', $name));
1021
        } elseif (!method_exists($className, 'send')) {
1022
            throw new InvalidArgumentException(sprintf('The "%s" does not have a send() method.', $className));
1023
        }
1024
1025
        unset($config['className']);
1026
        return new $className($config);
1027
    }
1028
1029
    /**
1030
     * Message-ID
1031
     *
1032
     * @param bool|string|null $message True to generate a new Message-ID, False to ignore (not send in email), String to set as Message-ID
1033
     * @return bool|string|$this
1034
     * @throws \InvalidArgumentException
1035
     */
1036
    public function messageId($message = null)
1037
    {
1038
        if ($message === null) {
1039
            return $this->_messageId;
1040
        }
1041
        if (is_bool($message)) {
1042
            $this->_messageId = $message;
1043
        } else {
1044
            if (!preg_match('/^\<.+@.+\>$/', $message)) {
1045
                throw new InvalidArgumentException('Invalid format to Message-ID. The text should be something like "<[email protected]>"');
1046
            }
1047
            $this->_messageId = $message;
1048
        }
1049
        return $this;
1050
    }
1051
1052
    /**
1053
     * Domain as top level (the part after @)
1054
     *
1055
     * @param string|null $domain Manually set the domain for CLI mailing
1056
     * @return string|$this
1057
     */
1058
    public function domain($domain = null)
1059
    {
1060
        if ($domain === null) {
1061
            return $this->_domain;
1062
        }
1063
        $this->_domain = $domain;
1064
        return $this;
1065
    }
1066
1067
    /**
1068
     * Add attachments to the email message
1069
     *
1070
     * Attachments can be defined in a few forms depending on how much control you need:
1071
     *
1072
     * Attach a single file:
1073
     *
1074
     * ```
1075
     * $email->attachments('path/to/file');
1076
     * ```
1077
     *
1078
     * Attach a file with a different filename:
1079
     *
1080
     * ```
1081
     * $email->attachments(['custom_name.txt' => 'path/to/file.txt']);
1082
     * ```
1083
     *
1084
     * Attach a file and specify additional properties:
1085
     *
1086
     * ```
1087
     * $email->attachments(['custom_name.png' => [
1088
     *      'file' => 'path/to/file',
1089
     *      'mimetype' => 'image/png',
1090
     *      'contentId' => 'abc123',
1091
     *      'contentDisposition' => false
1092
     *    ]
1093
     * ]);
1094
     * ```
1095
     *
1096
     * Attach a file from string and specify additional properties:
1097
     *
1098
     * ```
1099
     * $email->attachments(['custom_name.png' => [
1100
     *      'data' => file_get_contents('path/to/file'),
1101
     *      'mimetype' => 'image/png'
1102
     *    ]
1103
     * ]);
1104
     * ```
1105
     *
1106
     * The `contentId` key allows you to specify an inline attachment. In your email text, you
1107
     * can use `<img src="cid:abc123" />` to display the image inline.
1108
     *
1109
     * The `contentDisposition` key allows you to disable the `Content-Disposition` header, this can improve
1110
     * attachment compatibility with outlook email clients.
1111
     *
1112
     * @param string|array|null $attachments String with the filename or array with filenames
1113
     * @return array|$this Either the array of attachments when getting or $this when setting.
1114
     * @throws \InvalidArgumentException
1115
     */
1116
    public function attachments($attachments = null)
1117
    {
1118
        if ($attachments === null) {
1119
            return $this->_attachments;
1120
        }
1121
        $attach = [];
1122
        foreach ((array)$attachments as $name => $fileInfo) {
1123
            if (!is_array($fileInfo)) {
1124
                $fileInfo = ['file' => $fileInfo];
1125
            }
1126
            if (!isset($fileInfo['file'])) {
1127
                if (!isset($fileInfo['data'])) {
1128
                    throw new InvalidArgumentException('No file or data specified.');
1129
                }
1130
                if (is_int($name)) {
1131
                    throw new InvalidArgumentException('No filename specified.');
1132
                }
1133
                $fileInfo['data'] = chunk_split(base64_encode($fileInfo['data']), 76, "\r\n");
1134
            } else {
1135
                $fileName = $fileInfo['file'];
1136
                $fileInfo['file'] = realpath($fileInfo['file']);
1137
                if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) {
1138
                    throw new InvalidArgumentException(sprintf('File not found: "%s"', $fileName));
1139
                }
1140
                if (is_int($name)) {
1141
                    $name = basename($fileInfo['file']);
1142
                }
1143
            }
1144
            if (!isset($fileInfo['mimetype'])) {
1145
                $fileInfo['mimetype'] = 'application/octet-stream';
1146
            }
1147
            $attach[$name] = $fileInfo;
1148
        }
1149
        $this->_attachments = $attach;
1150
        return $this;
1151
    }
1152
1153
    /**
1154
     * Add attachments
1155
     *
1156
     * @param string|array $attachments String with the filename or array with filenames
1157
     * @return $this
1158
     * @throws \InvalidArgumentException
1159
     * @see \Cake\Mailer\Email::attachments()
1160
     */
1161
    public function addAttachments($attachments)
1162
    {
1163
        $current = $this->_attachments;
1164
        $this->attachments($attachments);
1165
        $this->_attachments = array_merge($current, $this->_attachments);
1166
        return $this;
1167
    }
1168
1169
    /**
1170
     * Get generated message (used by transport classes)
1171
     *
1172
     * @param string|null $type Use MESSAGE_* constants or null to return the full message as array
1173
     * @return string|array String if have type, array if type is null
1174
     */
1175
    public function message($type = null)
1176
    {
1177
        switch ($type) {
1178
            case static::MESSAGE_HTML:
1179
                return $this->_htmlMessage;
1180
            case static::MESSAGE_TEXT:
1181
                return $this->_textMessage;
1182
        }
1183
        return $this->_message;
1184
    }
1185
1186
    /**
1187
     * Add or read transport configuration.
1188
     *
1189
     * Use this method to define transports to use in delivery profiles.
1190
     * Once defined you cannot edit the configurations, and must use
1191
     * Email::dropTransport() to flush the configuration first.
1192
     *
1193
     * When using an array of configuration data a new transport
1194
     * will be constructed for each message sent. When using a Closure, the
1195
     * closure will be evaluated for each message.
1196
     *
1197
     * The `className` is used to define the class to use for a transport.
1198
     * It can either be a short name, or a fully qualified classname
1199
     *
1200
     * @param string|array $key The configuration name to read/write. Or
1201
     *   an array of multiple transports to set.
1202
     * @param array|AbstractTransport|null $config Either an array of configuration
1203
     *   data, or a transport instance.
1204
     * @return mixed Either null when setting or an array of data when reading.
1205
     * @throws \BadMethodCallException When modifying an existing configuration.
1206
     */
1207
    public static function configTransport($key, $config = null)
1208
    {
1209
        if ($config === null && is_string($key)) {
1210
            return isset(static::$_transportConfig[$key]) ? static::$_transportConfig[$key] : null;
1211
        }
1212
        if ($config === null && is_array($key)) {
1213
            foreach ($key as $name => $settings) {
1214
                static::configTransport($name, $settings);
1215
            }
1216
            return;
1217
        }
1218
        if (isset(static::$_transportConfig[$key])) {
1219
            throw new BadMethodCallException(sprintf('Cannot modify an existing config "%s"', $key));
1220
        }
1221
1222
        if (is_object($config)) {
1223
            $config = ['className' => $config];
1224
        }
1225
1226 View Code Duplication
        if (isset($config['url'])) {
1227
            $parsed = static::parseDsn($config['url']);
1228
            unset($config['url']);
1229
            $config = $parsed + $config;
1230
        }
1231
1232
        static::$_transportConfig[$key] = $config;
1233
    }
1234
1235
    /**
1236
     * Returns an array containing the named transport configurations
1237
     *
1238
     * @return array Array of configurations.
1239
     */
1240
    public static function configuredTransport()
1241
    {
1242
        return array_keys(static::$_transportConfig);
1243
    }
1244
1245
    /**
1246
     * Delete transport configuration.
1247
     *
1248
     * @param string $key The transport name to remove.
1249
     * @return void
1250
     */
1251
    public static function dropTransport($key)
1252
    {
1253
        unset(static::$_transportConfig[$key]);
1254
    }
1255
1256
    /**
1257
     * Get/Set the configuration profile to use for this instance.
1258
     *
1259
     * @param null|string|array $config String with configuration name, or
1260
     *    an array with config or null to return current config.
1261
     * @return string|array|$this
1262
     */
1263
    public function profile($config = null)
1264
    {
1265
        if ($config === null) {
1266
            return $this->_profile;
1267
        }
1268
        if (!is_array($config)) {
1269
            $config = (string)$config;
1270
        }
1271
        $this->_applyConfig($config);
1272
        return $this;
1273
    }
1274
1275
    /**
1276
     * Send an email using the specified content, template and layout
1277
     *
1278
     * @param string|array|null $content String with message or array with messages
1279
     * @return array
1280
     * @throws \BadMethodCallException
1281
     */
1282
    public function send($content = null)
1283
    {
1284
        if (empty($this->_from)) {
1285
            throw new BadMethodCallException('From is not specified.');
1286
        }
1287
        if (empty($this->_to) && empty($this->_cc) && empty($this->_bcc)) {
1288
            throw new BadMethodCallException('You need specify one destination on to, cc or bcc.');
1289
        }
1290
1291
        if (is_array($content)) {
1292
            $content = implode("\n", $content) . "\n";
1293
        }
1294
1295
        $this->_message = $this->_render($this->_wrap($content));
1296
1297
        $transport = $this->transport();
1298
        if (!$transport) {
1299
            $msg = 'Cannot send email, transport was not defined. Did you call transport() or define ' .
1300
                ' a transport in the set profile?';
1301
            throw new BadMethodCallException($msg);
1302
        }
1303
        $contents = $transport->send($this);
1304
        $this->_logDelivery($contents);
1305
        return $contents;
1306
    }
1307
1308
    /**
1309
     * Log the email message delivery.
1310
     *
1311
     * @param array $contents The content with 'headers' and 'message' keys.
1312
     * @return void
1313
     */
1314
    protected function _logDelivery($contents)
1315
    {
1316
        if (empty($this->_profile['log'])) {
1317
            return;
1318
        }
1319
        $config = [
1320
            'level' => 'debug',
1321
            'scope' => 'email'
1322
        ];
1323
        if ($this->_profile['log'] !== true) {
1324
            if (!is_array($this->_profile['log'])) {
1325
                $this->_profile['log'] = ['level' => $this->_profile['log']];
1326
            }
1327
            $config = $this->_profile['log'] + $config;
1328
        }
1329
        Log::write(
1330
            $config['level'],
1331
            PHP_EOL . $contents['headers'] . PHP_EOL . $contents['message'],
1332
            $config['scope']
1333
        );
1334
    }
1335
1336
    /**
1337
     * Static method to fast create an instance of \Cake\Mailer\Email
1338
     *
1339
     * @param string|array $to Address to send (see Cake\Mailer\Email::to()). If null, will try to use 'to' from transport config
1340
     * @param string $subject String of subject or null to use 'subject' from transport config
1341
     * @param string|array $message String with message or array with variables to be used in render
1342
     * @param string|array $transportConfig String to use config from EmailConfig or array with configs
1343
     * @param bool $send Send the email or just return the instance pre-configured
1344
     * @return \Cake\Mailer\Email Instance of Cake\Mailer\Email
1345
     * @throws \InvalidArgumentException
1346
     */
1347
    public static function deliver($to = null, $subject = null, $message = null, $transportConfig = 'fast', $send = true)
1348
    {
1349
        $class = __CLASS__;
1350
        $instance = new $class($transportConfig);
1351
        if ($to !== null) {
1352
            $instance->to($to);
1353
        }
1354
        if ($subject !== null) {
1355
            $instance->subject($subject);
1356
        }
1357
        if (is_array($message)) {
1358
            $instance->viewVars($message);
1359
            $message = null;
1360
        } elseif ($message === null && array_key_exists('message', $config = $instance->profile())) {
1361
            $message = $config['message'];
1362
        }
1363
1364
        if ($send === true) {
1365
            $instance->send($message);
1366
        }
1367
1368
        return $instance;
1369
    }
1370
1371
    /**
1372
     * Apply the config to an instance
1373
     *
1374
     * @param string|array $config Configuration options.
1375
     * @return void
1376
     * @throws \InvalidArgumentException When using a configuration that doesn't exist.
1377
     */
1378
    protected function _applyConfig($config)
1379
    {
1380 View Code Duplication
        if (is_string($config)) {
1381
            $name = $config;
1382
            $config = static::config($name);
1383
            if (empty($config)) {
1384
                throw new InvalidArgumentException(sprintf('Unknown email configuration "%s".', $name));
1385
            }
1386
            unset($name);
1387
        }
1388
1389
        $this->_profile = array_merge($this->_profile, $config);
1390
1391
        $simpleMethods = [
1392
            'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath',
1393
            'cc', 'bcc', 'messageId', 'domain', 'subject', 'attachments',
1394
            'transport', 'emailFormat', 'emailPattern', 'charset', 'headerCharset'
1395
        ];
1396
        foreach ($simpleMethods as $method) {
1397
            if (isset($config[$method])) {
1398
                $this->$method($config[$method]);
1399
            }
1400
        }
1401
1402
        if (empty($this->headerCharset)) {
1403
            $this->headerCharset = $this->charset;
1404
        }
1405
        if (isset($config['headers'])) {
1406
            $this->setHeaders($config['headers']);
1407
        }
1408
1409
        $viewBuilderMethods = [
1410
            'template', 'layout', 'theme'
1411
        ];
1412
        foreach ($viewBuilderMethods as $method) {
1413
            if (array_key_exists($method, $config)) {
1414
                $this->viewBuilder()->$method($config[$method]);
1415
            }
1416
        }
1417
1418
        if (array_key_exists('helpers', $config)) {
1419
            $this->viewBuilder()->helpers($config['helpers'], false);
1420
        }
1421
        if (array_key_exists('viewRender', $config)) {
1422
            $this->viewBuilder()->className($config['viewRender']);
1423
        }
1424
        if (array_key_exists('viewVars', $config)) {
1425
            $this->set($config['viewVars']);
1426
        }
1427
    }
1428
1429
    /**
1430
     * Reset all the internal variables to be able to send out a new email.
1431
     *
1432
     * @return $this
1433
     */
1434
    public function reset()
1435
    {
1436
        $this->_to = [];
1437
        $this->_from = [];
1438
        $this->_sender = [];
1439
        $this->_replyTo = [];
1440
        $this->_readReceipt = [];
1441
        $this->_returnPath = [];
1442
        $this->_cc = [];
1443
        $this->_bcc = [];
1444
        $this->_messageId = true;
1445
        $this->_subject = '';
1446
        $this->_headers = [];
1447
        $this->_textMessage = '';
1448
        $this->_htmlMessage = '';
1449
        $this->_message = '';
0 ignored issues
show
Documentation Bug introduced by
It seems like '' of type string is incompatible with the declared type array of property $_message.

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...
1450
        $this->_emailFormat = 'text';
1451
        $this->_transport = null;
1452
        $this->charset = 'utf-8';
1453
        $this->headerCharset = null;
1454
        $this->_attachments = [];
1455
        $this->_profile = [];
1456
        $this->_emailPattern = self::EMAIL_PATTERN;
1457
1458
        $this->viewBuilder()->layout('default');
1459
        $this->viewBuilder()->template('');
1460
        $this->viewBuilder()->classname('Cake\View\View');
1461
        $this->viewVars = [];
1462
        $this->viewBuilder()->theme(false);
1463
        $this->viewBuilder()->helpers(['Html'], false);
1464
1465
        return $this;
1466
    }
1467
1468
    /**
1469
     * Encode the specified string using the current charset
1470
     *
1471
     * @param string $text String to encode
1472
     * @return string Encoded string
1473
     */
1474
    protected function _encode($text)
1475
    {
1476
        $restore = mb_internal_encoding();
1477
        mb_internal_encoding($this->_appCharset);
1478
        if (empty($this->headerCharset)) {
1479
            $this->headerCharset = $this->charset;
1480
        }
1481
        $return = mb_encode_mimeheader($text, $this->headerCharset, 'B');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $return is correct as mb_encode_mimeheader($te...is->headerCharset, 'B') (which targets mb_encode_mimeheader()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1482
        mb_internal_encoding($restore);
1483
        return $return;
1484
    }
1485
1486
    /**
1487
     * Translates a string for one charset to another if the App.encoding value
1488
     * differs and the mb_convert_encoding function exists
1489
     *
1490
     * @param string $text The text to be converted
1491
     * @param string $charset the target encoding
1492
     * @return string
1493
     */
1494
    protected function _encodeString($text, $charset)
1495
    {
1496
        if ($this->_appCharset === $charset) {
1497
            return $text;
1498
        }
1499
        return mb_convert_encoding($text, $charset, $this->_appCharset);
1500
    }
1501
1502
    /**
1503
     * Wrap the message to follow the RFC 2822 - 2.1.1
1504
     *
1505
     * @param string $message Message to wrap
1506
     * @param int $wrapLength The line length
1507
     * @return array Wrapped message
1508
     */
1509
    protected function _wrap($message, $wrapLength = Email::LINE_LENGTH_MUST)
1510
    {
1511
        if (strlen($message) === 0) {
1512
            return [''];
1513
        }
1514
        $message = str_replace(["\r\n", "\r"], "\n", $message);
1515
        $lines = explode("\n", $message);
1516
        $formatted = [];
1517
        $cut = ($wrapLength == Email::LINE_LENGTH_MUST);
1518
1519
        foreach ($lines as $line) {
1520
            if (empty($line) && $line !== '0') {
1521
                $formatted[] = '';
1522
                continue;
1523
            }
1524
            if (strlen($line) < $wrapLength) {
1525
                $formatted[] = $line;
1526
                continue;
1527
            }
1528
            if (!preg_match('/<[a-z]+.*>/i', $line)) {
1529
                $formatted = array_merge(
1530
                    $formatted,
1531
                    explode("\n", wordwrap($line, $wrapLength, "\n", $cut))
1532
                );
1533
                continue;
1534
            }
1535
1536
            $tagOpen = false;
1537
            $tmpLine = $tag = '';
1538
            $tmpLineLength = 0;
1539
            for ($i = 0, $count = strlen($line); $i < $count; $i++) {
1540
                $char = $line[$i];
1541
                if ($tagOpen) {
1542
                    $tag .= $char;
1543
                    if ($char === '>') {
1544
                        $tagLength = strlen($tag);
1545
                        if ($tagLength + $tmpLineLength < $wrapLength) {
1546
                            $tmpLine .= $tag;
1547
                            $tmpLineLength += $tagLength;
1548
                        } else {
1549
                            if ($tmpLineLength > 0) {
1550
                                $formatted = array_merge(
1551
                                    $formatted,
1552
                                    explode("\n", wordwrap(trim($tmpLine), $wrapLength, "\n", $cut))
1553
                                );
1554
                                $tmpLine = '';
1555
                                $tmpLineLength = 0;
1556
                            }
1557
                            if ($tagLength > $wrapLength) {
1558
                                $formatted[] = $tag;
1559
                            } else {
1560
                                $tmpLine = $tag;
1561
                                $tmpLineLength = $tagLength;
1562
                            }
1563
                        }
1564
                        $tag = '';
1565
                        $tagOpen = false;
1566
                    }
1567
                    continue;
1568
                }
1569
                if ($char === '<') {
1570
                    $tagOpen = true;
1571
                    $tag = '<';
1572
                    continue;
1573
                }
1574
                if ($char === ' ' && $tmpLineLength >= $wrapLength) {
1575
                    $formatted[] = $tmpLine;
1576
                    $tmpLineLength = 0;
1577
                    continue;
1578
                }
1579
                $tmpLine .= $char;
1580
                $tmpLineLength++;
1581
                if ($tmpLineLength === $wrapLength) {
1582
                    $nextChar = $line[$i + 1];
1583
                    if ($nextChar === ' ' || $nextChar === '<') {
1584
                        $formatted[] = trim($tmpLine);
1585
                        $tmpLine = '';
1586
                        $tmpLineLength = 0;
1587
                        if ($nextChar === ' ') {
1588
                            $i++;
1589
                        }
1590
                    } else {
1591
                        $lastSpace = strrpos($tmpLine, ' ');
1592
                        if ($lastSpace === false) {
1593
                            continue;
1594
                        }
1595
                        $formatted[] = trim(substr($tmpLine, 0, $lastSpace));
1596
                        $tmpLine = substr($tmpLine, $lastSpace + 1);
1597
1598
                        $tmpLineLength = strlen($tmpLine);
1599
                    }
1600
                }
1601
            }
1602
            if (!empty($tmpLine)) {
1603
                $formatted[] = $tmpLine;
1604
            }
1605
        }
1606
        $formatted[] = '';
1607
        return $formatted;
1608
    }
1609
1610
    /**
1611
     * Create unique boundary identifier
1612
     *
1613
     * @return void
1614
     */
1615
    protected function _createBoundary()
1616
    {
1617
        if (!empty($this->_attachments) || $this->_emailFormat === 'both') {
1618
            $this->_boundary = md5(uniqid(time()));
1619
        }
1620
    }
1621
1622
    /**
1623
     * Attach non-embedded files by adding file contents inside boundaries.
1624
     *
1625
     * @param string|null $boundary Boundary to use. If null, will default to $this->_boundary
1626
     * @return array An array of lines to add to the message
1627
     */
1628
    protected function _attachFiles($boundary = null)
1629
    {
1630
        if ($boundary === null) {
1631
            $boundary = $this->_boundary;
1632
        }
1633
1634
        $msg = [];
1635
        foreach ($this->_attachments as $filename => $fileInfo) {
1636
            if (!empty($fileInfo['contentId'])) {
1637
                continue;
1638
            }
1639
            $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']);
1640
            $hasDisposition = (
1641
                !isset($fileInfo['contentDisposition']) ||
1642
                $fileInfo['contentDisposition']
1643
            );
1644
            $part = new Part(false, $data, false);
1645
1646
            if ($hasDisposition) {
1647
                $part->disposition('attachment');
1648
                $part->filename($filename);
1649
            }
1650
            $part->transferEncoding('base64');
1651
            $part->type($fileInfo['mimetype']);
1652
1653
            $msg[] = '--' . $boundary;
1654
            $msg[] = (string)$part;
1655
            $msg[] = '';
1656
        }
1657
        return $msg;
1658
    }
1659
1660
    /**
1661
     * Read the file contents and return a base64 version of the file contents.
1662
     *
1663
     * @param string $path The absolute path to the file to read.
1664
     * @return string File contents in base64 encoding
1665
     */
1666
    protected function _readFile($path)
1667
    {
1668
        $File = new File($path);
1669
        return chunk_split(base64_encode($File->read()));
1670
    }
1671
1672
    /**
1673
     * Attach inline/embedded files to the message.
1674
     *
1675
     * @param string|null $boundary Boundary to use. If null, will default to $this->_boundary
1676
     * @return array An array of lines to add to the message
1677
     */
1678
    protected function _attachInlineFiles($boundary = null)
1679
    {
1680
        if ($boundary === null) {
1681
            $boundary = $this->_boundary;
1682
        }
1683
1684
        $msg = [];
1685
        foreach ($this->_attachments as $filename => $fileInfo) {
1686
            if (empty($fileInfo['contentId'])) {
1687
                continue;
1688
            }
1689
            $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']);
1690
1691
            $msg[] = '--' . $boundary;
1692
            $part = new Part(false, $data, 'inline');
1693
            $part->type($fileInfo['mimetype']);
1694
            $part->transferEncoding('base64');
1695
            $part->contentId($fileInfo['contentId']);
1696
            $part->filename($filename);
1697
            $msg[] = (string)$part;
1698
            $msg[] = '';
1699
        }
1700
        return $msg;
1701
    }
1702
1703
    /**
1704
     * Render the body of the email.
1705
     *
1706
     * @param array $content Content to render
1707
     * @return array Email body ready to be sent
1708
     */
1709
    protected function _render($content)
1710
    {
1711
        $this->_textMessage = $this->_htmlMessage = '';
1712
1713
        $content = implode("\n", $content);
1714
        $rendered = $this->_renderTemplates($content);
1715
1716
        $this->_createBoundary();
1717
        $msg = [];
1718
1719
        $contentIds = array_filter((array)Hash::extract($this->_attachments, '{s}.contentId'));
1720
        $hasInlineAttachments = count($contentIds) > 0;
1721
        $hasAttachments = !empty($this->_attachments);
1722
        $hasMultipleTypes = count($rendered) > 1;
1723
        $multiPart = ($hasAttachments || $hasMultipleTypes);
1724
1725
        $boundary = $relBoundary = $textBoundary = $this->_boundary;
1726
1727 View Code Duplication
        if ($hasInlineAttachments) {
1728
            $msg[] = '--' . $boundary;
1729
            $msg[] = 'Content-Type: multipart/related; boundary="rel-' . $boundary . '"';
1730
            $msg[] = '';
1731
            $relBoundary = $textBoundary = 'rel-' . $boundary;
1732
        }
1733
1734 View Code Duplication
        if ($hasMultipleTypes && $hasAttachments) {
1735
            $msg[] = '--' . $relBoundary;
1736
            $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"';
1737
            $msg[] = '';
1738
            $textBoundary = 'alt-' . $boundary;
1739
        }
1740
1741 View Code Duplication
        if (isset($rendered['text'])) {
1742
            if ($multiPart) {
1743
                $msg[] = '--' . $textBoundary;
1744
                $msg[] = 'Content-Type: text/plain; charset=' . $this->_getContentTypeCharset();
1745
                $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding();
1746
                $msg[] = '';
1747
            }
1748
            $this->_textMessage = $rendered['text'];
1749
            $content = explode("\n", $this->_textMessage);
1750
            $msg = array_merge($msg, $content);
1751
            $msg[] = '';
1752
        }
1753
1754 View Code Duplication
        if (isset($rendered['html'])) {
1755
            if ($multiPart) {
1756
                $msg[] = '--' . $textBoundary;
1757
                $msg[] = 'Content-Type: text/html; charset=' . $this->_getContentTypeCharset();
1758
                $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding();
1759
                $msg[] = '';
1760
            }
1761
            $this->_htmlMessage = $rendered['html'];
1762
            $content = explode("\n", $this->_htmlMessage);
1763
            $msg = array_merge($msg, $content);
1764
            $msg[] = '';
1765
        }
1766
1767
        if ($textBoundary !== $relBoundary) {
1768
            $msg[] = '--' . $textBoundary . '--';
1769
            $msg[] = '';
1770
        }
1771
1772
        if ($hasInlineAttachments) {
1773
            $attachments = $this->_attachInlineFiles($relBoundary);
1774
            $msg = array_merge($msg, $attachments);
1775
            $msg[] = '';
1776
            $msg[] = '--' . $relBoundary . '--';
1777
            $msg[] = '';
1778
        }
1779
1780
        if ($hasAttachments) {
1781
            $attachments = $this->_attachFiles($boundary);
1782
            $msg = array_merge($msg, $attachments);
1783
        }
1784
        if ($hasAttachments || $hasMultipleTypes) {
1785
            $msg[] = '';
1786
            $msg[] = '--' . $boundary . '--';
1787
            $msg[] = '';
1788
        }
1789
        return $msg;
1790
    }
1791
1792
    /**
1793
     * Gets the text body types that are in this email message
1794
     *
1795
     * @return array Array of types. Valid types are 'text' and 'html'
1796
     */
1797
    protected function _getTypes()
1798
    {
1799
        $types = [$this->_emailFormat];
1800
        if ($this->_emailFormat === 'both') {
1801
            $types = ['html', 'text'];
1802
        }
1803
        return $types;
1804
    }
1805
1806
    /**
1807
     * Build and set all the view properties needed to render the templated emails.
1808
     * If there is no template set, the $content will be returned in a hash
1809
     * of the text content types for the email.
1810
     *
1811
     * @param string $content The content passed in from send() in most cases.
1812
     * @return array The rendered content with html and text keys.
1813
     */
1814
    protected function _renderTemplates($content)
1815
    {
1816
        $types = $this->_getTypes();
1817
        $rendered = [];
1818
        $template = $this->viewBuilder()->template();
1819
        if (empty($template)) {
1820
            foreach ($types as $type) {
1821
                $rendered[$type] = $this->_encodeString($content, $this->charset);
1822
            }
1823
            return $rendered;
1824
        }
1825
1826
        $View = $this->createView();
1827
1828
        list($templatePlugin) = pluginSplit($View->template());
1829
        list($layoutPlugin) = pluginSplit($View->layout());
1830
        if ($templatePlugin) {
1831
            $View->plugin = $templatePlugin;
1832
        } elseif ($layoutPlugin) {
1833
            $View->plugin = $layoutPlugin;
1834
        }
1835
1836
        if ($View->get('content') === null) {
1837
            $View->set('content', $content);
1838
        }
1839
1840
        foreach ($types as $type) {
1841
            $View->hasRendered = false;
1842
            $View->templatePath('Email' . DIRECTORY_SEPARATOR . $type);
1843
            $View->layoutPath('Email' . DIRECTORY_SEPARATOR . $type);
1844
1845
            $render = $View->render();
1846
            $render = str_replace(["\r\n", "\r"], "\n", $render);
1847
            $rendered[$type] = $this->_encodeString($render, $this->charset);
1848
        }
1849
1850
        foreach ($rendered as $type => $content) {
1851
            $rendered[$type] = $this->_wrap($content);
1852
            $rendered[$type] = implode("\n", $rendered[$type]);
1853
            $rendered[$type] = rtrim($rendered[$type], "\n");
1854
        }
1855
        return $rendered;
1856
    }
1857
1858
    /**
1859
     * Return the Content-Transfer Encoding value based on the set charset
1860
     *
1861
     * @return string
1862
     */
1863
    protected function _getContentTransferEncoding()
1864
    {
1865
        $charset = strtoupper($this->charset);
1866
        if (in_array($charset, $this->_charset8bit)) {
1867
            return '8bit';
1868
        }
1869
        return '7bit';
1870
    }
1871
1872
    /**
1873
     * Return charset value for Content-Type.
1874
     *
1875
     * Checks fallback/compatibility types which include workarounds
1876
     * for legacy japanese character sets.
1877
     *
1878
     * @return string
1879
     */
1880
    protected function _getContentTypeCharset()
1881
    {
1882
        $charset = strtoupper($this->charset);
1883
        if (array_key_exists($charset, $this->_contentTypeCharset)) {
1884
            return strtoupper($this->_contentTypeCharset[$charset]);
1885
        }
1886
        return strtoupper($this->charset);
1887
    }
1888
1889
    /**
1890
     * Serializes the email object to a value that can be natively serialized and re-used
1891
     * to clone this email instance.
1892
     *
1893
     * It has certain limitations for viewVars that are good to know:
1894
     *
1895
     *    - ORM\Query executed and stored as resultset
1896
     *    - SimpleXmlElements stored as associative array
1897
     *    - Exceptions stored as strings
1898
     *    - Resources, \Closure and \PDO are not supported.
1899
     *
1900
     * @return array Serializable array of configuration properties.
1901
     * @throws \Exception When a view var object can not be properly serialized.
1902
     */
1903
    public function jsonSerialize()
1904
    {
1905
        $properties = [
1906
            '_to', '_from', '_sender', '_replyTo', '_cc', '_bcc', '_subject',
1907
            '_returnPath', '_readReceipt', '_emailFormat', '_emailPattern', '_domain',
1908
            '_attachments', '_messageId', '_headers', '_appCharset', 'viewVars', 'charset', 'headerCharset'
1909
        ];
1910
1911
        $array = ['viewConfig' => $this->viewBuilder()->jsonSerialize()];
1912
1913
        foreach ($properties as $property) {
1914
            $array[$property] = $this->{$property};
1915
        }
1916
1917
        array_walk($array['_attachments'], function (&$item, $key) {
1918
            if (!empty($item['file'])) {
1919
                $item['data'] = $this->_readFile($item['file']);
1920
                unset($item['file']);
1921
            }
1922
        });
1923
1924
        array_walk_recursive($array['viewVars'], [$this, '_checkViewVars']);
1925
1926
        return array_filter($array, function ($i) {
1927
            return !is_array($i) && strlen($i) || !empty($i);
1928
        });
1929
    }
1930
1931
    /**
1932
     * Iterates through hash to clean up and normalize.
1933
     *
1934
     * @param mixed $item Reference to the view var value.
1935
     * @param string $key View var key.
1936
     * @return void
1937
     */
1938
    protected function _checkViewVars(&$item, $key)
1939
    {
1940
        if ($item instanceof Exception) {
1941
            $item = (string)$item;
1942
        }
1943
1944
        if (is_resource($item) ||
1945
            $item instanceof Closure ||
1946
            $item instanceof PDO
1947
        ) {
1948
            throw new RuntimeException(sprintf(
1949
                'Failed serializing the `%s` %s in the `%s` view var',
1950
                is_resource($item) ? get_resource_type($item) : get_class($item),
1951
                is_resource($item) ? 'resource' : 'object',
1952
                $key
1953
            ));
1954
        }
1955
    }
1956
1957
    /**
1958
     * Configures an email instance object from serialized config.
1959
     *
1960
     * @param array $config Email configuration array.
1961
     * @return \Cake\Mailer\Email Configured email instance.
1962
     */
1963
    public function createFromArray($config)
1964
    {
1965
        if (isset($config['viewConfig'])) {
1966
            $this->viewBuilder()->createFromArray($config['viewConfig']);
1967
            unset($config['viewConfig']);
1968
        }
1969
1970
        foreach ($config as $property => $value) {
1971
            $this->{$property} = $value;
1972
        }
1973
1974
        return $this;
1975
    }
1976
1977
    /**
1978
     * Serializes the Email object.
1979
     *
1980
     * @return string
1981
     */
1982
    public function serialize()
1983
    {
1984
        $array = $this->jsonSerialize();
1985
        array_walk_recursive($array, function (&$item, $key) {
1986
            if ($item instanceof SimpleXmlElement) {
1987
                $item = json_decode(json_encode((array)$item), true);
1988
            }
1989
        });
1990
        return serialize($array);
1991
    }
1992
1993
    /**
1994
     * Unserializes the Email object.
1995
     *
1996
     * @param string $data Serialized string.
1997
     * @return \Cake\Mailer\Email Configured email instance.
1998
     */
1999
    public function unserialize($data)
2000
    {
2001
        return $this->createFromArray(unserialize($data));
2002
    }
2003
}
2004