Passed
Push — master ( 9e05e7...9b6fe5 )
by Marcus
07:17
created

PHPMailer::attachAll()   D

Complexity

Conditions 14
Paths 134

Size

Total Lines 109
Code Lines 71

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 109
rs 4.6283
c 0
b 0
f 0
cc 14
eloc 71
nc 134
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * PHPMailer - PHP email creation and transport class.
4
 * PHP Version 5.5.
5
 *
6
 * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
7
 *
8
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
9
 * @author    Jim Jagielski (jimjag) <[email protected]>
10
 * @author    Andy Prevost (codeworxtech) <[email protected]>
11
 * @author    Brent R. Matzelle (original founder)
12
 * @copyright 2012 - 2017 Marcus Bointon
13
 * @copyright 2010 - 2012 Jim Jagielski
14
 * @copyright 2004 - 2009 Andy Prevost
15
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
16
 * @note      This program is distributed in the hope that it will be useful - WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18
 * FITNESS FOR A PARTICULAR PURPOSE.
19
 */
20
21
namespace PHPMailer\PHPMailer;
22
23
/**
24
 * PHPMailer - PHP email creation and transport class.
25
 *
26
 * @author  Marcus Bointon (Synchro/coolbru) <[email protected]>
27
 * @author  Jim Jagielski (jimjag) <[email protected]>
28
 * @author  Andy Prevost (codeworxtech) <[email protected]>
29
 * @author  Brent R. Matzelle (original founder)
30
 */
31
class PHPMailer
0 ignored issues
show
Complexity introduced by
This class has 134 public methods and attributes which exceeds the configured maximum of 45.

The number of this metric differs depending on the chosen design (inheritance vs. composition). For inheritance, the number should generally be a bit lower.

A high number indicates a reusable class. It might also make the class harder to change without breaking other classes though.

Loading history...
Complexity introduced by
This class has 73 fields which exceeds the configured maximum of 15.

Too many fields generally indicate a class which does too much and does not follow the single responsibility principle.

We suggest taking a look at the “Code” section for further suggestions on how to fix this.

Loading history...
Complexity introduced by
This class has 4331 lines of code which exceeds the configured maximum of 1000.

Really long classes often contain too much logic and violate the single responsibility principle.

We suggest to take a look at the “Code” section for options on how to refactor this code.

Loading history...
Complexity introduced by
This class has a complexity of 533 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

Loading history...
32
{
33
    /**
34
     * Email priority.
35
     * Options: null (default), 1 = High, 3 = Normal, 5 = low.
36
     * When null, the header is not set at all.
37
     *
38
     * @var int
39
     */
40
    public $Priority = null;
41
42
    /**
43
     * The character set of the message.
44
     *
45
     * @var string
46
     */
47
    public $CharSet = 'iso-8859-1';
48
49
    /**
50
     * The MIME Content-type of the message.
51
     *
52
     * @var string
53
     */
54
    public $ContentType = 'text/plain';
55
56
    /**
57
     * The message encoding.
58
     * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
59
     *
60
     * @var string
61
     */
62
    public $Encoding = '8bit';
63
64
    /**
65
     * Holds the most recent mailer error message.
66
     *
67
     * @var string
68
     */
69
    public $ErrorInfo = '';
70
71
    /**
72
     * The From email address for the message.
73
     *
74
     * @var string
75
     */
76
    public $From = 'root@localhost';
77
78
    /**
79
     * The From name of the message.
80
     *
81
     * @var string
82
     */
83
    public $FromName = 'Root User';
84
85
    /**
86
     * The envelope sender of the message.
87
     * This will usually be turned into a Return-Path header by the receiver,
88
     * and is the address that bounces will be sent to.
89
     * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
90
     *
91
     * @var string
92
     */
93
    public $Sender = '';
94
95
    /**
96
     * The Subject of the message.
97
     *
98
     * @var string
99
     */
100
    public $Subject = '';
101
102
    /**
103
     * An HTML or plain text message body.
104
     * If HTML then call isHTML(true).
105
     *
106
     * @var string
107
     */
108
    public $Body = '';
109
110
    /**
111
     * The plain-text message body.
112
     * This body can be read by mail clients that do not have HTML email
113
     * capability such as mutt & Eudora.
114
     * Clients that can read HTML will view the normal Body.
115
     *
116
     * @var string
117
     */
118
    public $AltBody = '';
119
120
    /**
121
     * An iCal message part body.
122
     * Only supported in simple alt or alt_inline message types
123
     * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
124
     *
125
     * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/
126
     * @see http://kigkonsult.se/iCalcreator/
127
     *
128
     * @var string
129
     */
130
    public $Ical = '';
131
132
    /**
133
     * The complete compiled MIME message body.
134
     *
135
     * @var string
136
     */
137
    protected $MIMEBody = '';
138
139
    /**
140
     * The complete compiled MIME message headers.
141
     *
142
     * @var string
143
     */
144
    protected $MIMEHeader = '';
145
146
    /**
147
     * Extra headers that createHeader() doesn't fold in.
148
     *
149
     * @var string
150
     */
151
    protected $mailHeader = '';
152
153
    /**
154
     * Word-wrap the message body to this number of chars.
155
     * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
156
     *
157
     * @see static::STD_LINE_LENGTH
158
     *
159
     * @var int
160
     */
161
    public $WordWrap = 0;
162
163
    /**
164
     * Which method to use to send mail.
165
     * Options: "mail", "sendmail", or "smtp".
166
     *
167
     * @var string
168
     */
169
    public $Mailer = 'mail';
170
171
    /**
172
     * The path to the sendmail program.
173
     *
174
     * @var string
175
     */
176
    public $Sendmail = '/usr/sbin/sendmail';
177
178
    /**
179
     * Whether mail() uses a fully sendmail-compatible MTA.
180
     * One which supports sendmail's "-oi -f" options.
181
     *
182
     * @var bool
183
     */
184
    public $UseSendmailOptions = true;
185
186
    /**
187
     * The email address that a reading confirmation should be sent to, also known as read receipt.
188
     *
189
     * @var string
190
     */
191
    public $ConfirmReadingTo = '';
192
193
    /**
194
     * The hostname to use in the Message-ID header and as default HELO string.
195
     * If empty, PHPMailer attempts to find one with, in order,
196
     * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
197
     * 'localhost.localdomain'.
198
     *
199
     * @var string
200
     */
201
    public $Hostname = '';
202
203
    /**
204
     * An ID to be used in the Message-ID header.
205
     * If empty, a unique id will be generated.
206
     * You can set your own, but it must be in the format "<id@domain>",
207
     * as defined in RFC5322 section 3.6.4 or it will be ignored.
208
     *
209
     * @see https://tools.ietf.org/html/rfc5322#section-3.6.4
210
     *
211
     * @var string
212
     */
213
    public $MessageID = '';
214
215
    /**
216
     * The message Date to be used in the Date header.
217
     * If empty, the current date will be added.
218
     *
219
     * @var string
220
     */
221
    public $MessageDate = '';
222
223
    /**
224
     * SMTP hosts.
225
     * Either a single hostname or multiple semicolon-delimited hostnames.
226
     * You can also specify a different port
227
     * for each host by using this format: [hostname:port]
228
     * (e.g. "smtp1.example.com:25;smtp2.example.com").
229
     * You can also specify encryption type, for example:
230
     * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
231
     * Hosts will be tried in order.
232
     *
233
     * @var string
234
     */
235
    public $Host = 'localhost';
236
237
    /**
238
     * The default SMTP server port.
239
     *
240
     * @var int
241
     */
242
    public $Port = 25;
243
244
    /**
245
     * The SMTP HELO of the message.
246
     * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
247
     * one with the same method described above for $Hostname.
248
     *
249
     * @see PHPMailer::$Hostname
250
     *
251
     * @var string
252
     */
253
    public $Helo = '';
254
255
    /**
256
     * What kind of encryption to use on the SMTP connection.
257
     * Options: '', 'ssl' or 'tls'.
258
     *
259
     * @var string
260
     */
261
    public $SMTPSecure = '';
262
263
    /**
264
     * Whether to enable TLS encryption automatically if a server supports it,
265
     * even if `SMTPSecure` is not set to 'tls'.
266
     * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
267
     *
268
     * @var bool
269
     */
270
    public $SMTPAutoTLS = true;
271
272
    /**
273
     * Whether to use SMTP authentication.
274
     * Uses the Username and Password properties.
275
     *
276
     * @see PHPMailer::$Username
277
     * @see PHPMailer::$Password
278
     *
279
     * @var bool
280
     */
281
    public $SMTPAuth = false;
282
283
    /**
284
     * Options array passed to stream_context_create when connecting via SMTP.
285
     *
286
     * @var array
287
     */
288
    public $SMTPOptions = [];
289
290
    /**
291
     * SMTP username.
292
     *
293
     * @var string
294
     */
295
    public $Username = '';
296
297
    /**
298
     * SMTP password.
299
     *
300
     * @var string
301
     */
302
    public $Password = '';
303
304
    /**
305
     * SMTP auth type.
306
     * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified.
307
     *
308
     * @var string
309
     */
310
    public $AuthType = '';
311
312
    /**
313
     * An instance of the PHPMailer OAuth class.
314
     *
315
     * @var OAuth
316
     */
317
    protected $oauth = null;
318
319
    /**
320
     * The SMTP server timeout in seconds.
321
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
322
     *
323
     * @var int
324
     */
325
    public $Timeout = 300;
326
327
    /**
328
     * SMTP class debug output mode.
329
     * Debug output level.
330
     * Options:
331
     * * `0` No output
332
     * * `1` Commands
333
     * * `2` Data and commands
334
     * * `3` As 2 plus connection status
335
     * * `4` Low-level data output.
336
     *
337
     * @see SMTP::$do_debug
338
     *
339
     * @var int
340
     */
341
    public $SMTPDebug = 0;
342
343
    /**
344
     * How to handle debug output.
345
     * Options:
346
     * * `echo` Output plain-text as-is, appropriate for CLI
347
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
348
     * * `error_log` Output to error log as configured in php.ini
349
     * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
350
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
351
     *
352
     * ```php
353
     * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
354
     * ```
355
     *
356
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
357
     * level output is used:
358
     *
359
     * ```php
360
     * $mail->Debugoutput = new myPsr3Logger;
361
     * ```
362
     *
363
     * @see SMTP::$Debugoutput
364
     *
365
     * @var string|callable|\Psr\Log\LoggerInterface
366
     */
367
    public $Debugoutput = 'echo';
368
369
    /**
370
     * Whether to keep SMTP connection open after each message.
371
     * If this is set to true then to close the connection
372
     * requires an explicit call to smtpClose().
373
     *
374
     * @var bool
375
     */
376
    public $SMTPKeepAlive = false;
377
378
    /**
379
     * Whether to split multiple to addresses into multiple messages
380
     * or send them all in one message.
381
     * Only supported in `mail` and `sendmail` transports, not in SMTP.
382
     *
383
     * @var bool
384
     */
385
    public $SingleTo = false;
386
387
    /**
388
     * Storage for addresses when SingleTo is enabled.
389
     *
390
     * @var array
391
     */
392
    protected $SingleToArray = [];
393
394
    /**
395
     * Whether to generate VERP addresses on send.
396
     * Only applicable when sending via SMTP.
397
     *
398
     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
399
     * @see http://www.postfix.org/VERP_README.html Postfix VERP info
400
     *
401
     * @var bool
402
     */
403
    public $do_verp = false;
404
405
    /**
406
     * Whether to allow sending messages with an empty body.
407
     *
408
     * @var bool
409
     */
410
    public $AllowEmpty = false;
411
412
    /**
413
     * DKIM selector.
414
     *
415
     * @var string
416
     */
417
    public $DKIM_selector = '';
418
419
    /**
420
     * DKIM Identity.
421
     * Usually the email address used as the source of the email.
422
     *
423
     * @var string
424
     */
425
    public $DKIM_identity = '';
426
427
    /**
428
     * DKIM passphrase.
429
     * Used if your key is encrypted.
430
     *
431
     * @var string
432
     */
433
    public $DKIM_passphrase = '';
434
435
    /**
436
     * DKIM signing domain name.
437
     *
438
     * @example 'example.com'
439
     *
440
     * @var string
441
     */
442
    public $DKIM_domain = '';
443
444
    /**
445
     * DKIM private key file path.
446
     *
447
     * @var string
448
     */
449
    public $DKIM_private = '';
450
451
    /**
452
     * DKIM private key string.
453
     *
454
     * If set, takes precedence over `$DKIM_private`.
455
     *
456
     * @var string
457
     */
458
    public $DKIM_private_string = '';
459
460
    /**
461
     * Callback Action function name.
462
     *
463
     * The function that handles the result of the send email action.
464
     * It is called out by send() for each email sent.
465
     *
466
     * Value can be any php callable: http://www.php.net/is_callable
467
     *
468
     * Parameters:
469
     *   bool $result        result of the send action
470
     *   array   $to            email addresses of the recipients
471
     *   array   $cc            cc email addresses
472
     *   array   $bcc           bcc email addresses
473
     *   string  $subject       the subject
474
     *   string  $body          the email body
475
     *   string  $from          email address of sender
476
     *
477
     * @var string
478
     */
479
    public $action_function = '';
480
481
    /**
482
     * What to put in the X-Mailer header.
483
     * Options: An empty string for PHPMailer default, whitespace for none, or a string to use.
484
     *
485
     * @var string
486
     */
487
    public $XMailer = '';
488
489
    /**
490
     * Which validator to use by default when validating email addresses.
491
     * May be a callable to inject your own validator, but there are several built-in validators.
492
     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
493
     *
494
     * @see PHPMailer::validateAddress()
495
     *
496
     * @var string|callable
497
     */
498
    public static $validator = 'php';
499
500
    /**
501
     * An instance of the SMTP sender class.
502
     *
503
     * @var SMTP
504
     */
505
    protected $smtp = null;
506
507
    /**
508
     * The array of 'to' names and addresses.
509
     *
510
     * @var array
511
     */
512
    protected $to = [];
513
514
    /**
515
     * The array of 'cc' names and addresses.
516
     *
517
     * @var array
518
     */
519
    protected $cc = [];
520
521
    /**
522
     * The array of 'bcc' names and addresses.
523
     *
524
     * @var array
525
     */
526
    protected $bcc = [];
527
528
    /**
529
     * The array of reply-to names and addresses.
530
     *
531
     * @var array
532
     */
533
    protected $ReplyTo = [];
534
535
    /**
536
     * An array of all kinds of addresses.
537
     * Includes all of $to, $cc, $bcc.
538
     *
539
     * @see PHPMailer::$to
540
     * @see PHPMailer::$cc
541
     * @see PHPMailer::$bcc
542
     *
543
     * @var array
544
     */
545
    protected $all_recipients = [];
546
547
    /**
548
     * An array of names and addresses queued for validation.
549
     * In send(), valid and non duplicate entries are moved to $all_recipients
550
     * and one of $to, $cc, or $bcc.
551
     * This array is used only for addresses with IDN.
552
     *
553
     * @see PHPMailer::$to
554
     * @see PHPMailer::$cc
555
     * @see PHPMailer::$bcc
556
     * @see PHPMailer::$all_recipients
557
     *
558
     * @var array
559
     */
560
    protected $RecipientsQueue = [];
561
562
    /**
563
     * An array of reply-to names and addresses queued for validation.
564
     * In send(), valid and non duplicate entries are moved to $ReplyTo.
565
     * This array is used only for addresses with IDN.
566
     *
567
     * @see PHPMailer::$ReplyTo
568
     *
569
     * @var array
570
     */
571
    protected $ReplyToQueue = [];
572
573
    /**
574
     * The array of attachments.
575
     *
576
     * @var array
577
     */
578
    protected $attachment = [];
579
580
    /**
581
     * The array of custom headers.
582
     *
583
     * @var array
584
     */
585
    protected $CustomHeader = [];
586
587
    /**
588
     * The most recent Message-ID (including angular brackets).
589
     *
590
     * @var string
591
     */
592
    protected $lastMessageID = '';
593
594
    /**
595
     * The message's MIME type.
596
     *
597
     * @var string
598
     */
599
    protected $message_type = '';
600
601
    /**
602
     * The array of MIME boundary strings.
603
     *
604
     * @var array
605
     */
606
    protected $boundary = [];
607
608
    /**
609
     * The array of available languages.
610
     *
611
     * @var array
612
     */
613
    protected $language = [];
614
615
    /**
616
     * The number of errors encountered.
617
     *
618
     * @var int
619
     */
620
    protected $error_count = 0;
621
622
    /**
623
     * The S/MIME certificate file path.
624
     *
625
     * @var string
626
     */
627
    protected $sign_cert_file = '';
628
629
    /**
630
     * The S/MIME key file path.
631
     *
632
     * @var string
633
     */
634
    protected $sign_key_file = '';
635
636
    /**
637
     * The optional S/MIME extra certificates ("CA Chain") file path.
638
     *
639
     * @var string
640
     */
641
    protected $sign_extracerts_file = '';
642
643
    /**
644
     * The S/MIME password for the key.
645
     * Used only if the key is encrypted.
646
     *
647
     * @var string
648
     */
649
    protected $sign_key_pass = '';
650
651
    /**
652
     * Whether to throw exceptions for errors.
653
     *
654
     * @var bool
655
     */
656
    protected $exceptions = false;
657
658
    /**
659
     * Unique ID used for message ID and boundaries.
660
     *
661
     * @var string
662
     */
663
    protected $uniqueid = '';
664
665
    /**
666
     * The PHPMailer Version number.
667
     *
668
     * @var string
669
     */
670
    const VERSION = '6.0.0';
671
672
    /**
673
     * Error severity: message only, continue processing.
674
     *
675
     * @var int
676
     */
677
    const STOP_MESSAGE = 0;
678
679
    /**
680
     * Error severity: message, likely ok to continue processing.
681
     *
682
     * @var int
683
     */
684
    const STOP_CONTINUE = 1;
685
686
    /**
687
     * Error severity: message, plus full stop, critical error reached.
688
     *
689
     * @var int
690
     */
691
    const STOP_CRITICAL = 2;
692
693
    /**
694
     * SMTP RFC standard line ending.
695
     *
696
     * @var string
697
     */
698
    protected static $LE = "\r\n";
699
700
    /**
701
     * The maximum line length allowed by RFC 2822 section 2.1.1.
702
     *
703
     * @var int
704
     */
705
    const MAX_LINE_LENGTH = 998;
706
707
    /**
708
     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
709
     *
710
     * @var int
711
     */
712
    const STD_LINE_LENGTH = 78;
713
714
    /**
715
     * Constructor.
716
     *
717
     * @param bool $exceptions Should we throw external exceptions?
718
     */
719
    public function __construct($exceptions = null)
720
    {
721
        if (null !== $exceptions) {
722
            $this->exceptions = (bool) $exceptions;
723
        }
724
        //Pick an appropriate debug output format automatically
725
        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
726
    }
727
728
    /**
729
     * Destructor.
730
     */
731
    public function __destruct()
732
    {
733
        //Close any open SMTP connection nicely
734
        $this->smtpClose();
735
    }
736
737
    /**
738
     * Call mail() in a safe_mode-aware fashion.
739
     * Also, unless sendmail_path points to sendmail (or something that
740
     * claims to be sendmail), don't pass params (not a perfect fix,
741
     * but it will do).
742
     *
743
     * @param string      $to      To
744
     * @param string      $subject Subject
745
     * @param string      $body    Message Body
746
     * @param string      $header  Additional Header(s)
747
     * @param string|null $params  Params
748
     *
749
     * @return bool
750
     */
751
    private function mailPassthru($to, $subject, $body, $header, $params)
752
    {
753
        //Check overloading of mail function to avoid double-encoding
754
        if (ini_get('mbstring.func_overload') & 1) {
755
            $subject = $this->secureHeader($subject);
756
        } else {
757
            $subject = $this->encodeHeader($this->secureHeader($subject));
758
        }
759
        //Calling mail() with null params breaks
760
        if (!$this->UseSendmailOptions or null === $params) {
761
            $result = @mail($to, $subject, $body, $header);
762
        } else {
763
            $result = @mail($to, $subject, $body, $header, $params);
764
        }
765
766
        return $result;
767
    }
768
769
    /**
770
     * Output debugging info via user-defined method.
771
     * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug).
772
     *
773
     * @see PHPMailer::$Debugoutput
774
     * @see PHPMailer::$SMTPDebug
775
     *
776
     * @param string $str
777
     */
778
    protected function edebug($str)
779
    {
780
        if ($this->SMTPDebug <= 0) {
781
            return;
782
        }
783
        //Is this a PSR-3 logger?
784
        if (is_a($this->Debugoutput, 'Psr\Log\LoggerInterface')) {
785
            $this->Debugoutput->debug($str);
0 ignored issues
show
Bug introduced by
The method debug cannot be called on $this->Debugoutput (of type callable).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
786
787
            return;
788
        }
789
        //Avoid clash with built-in function names
790
        if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) {
791
            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
792
793
            return;
794
        }
795
        switch ($this->Debugoutput) {
796
            case 'error_log':
797
                //Don't output, just log
798
                error_log($str);
799
                break;
800
            case 'html':
801
                //Cleans up output a bit for a better looking, HTML-safe output
802
                echo htmlentities(
803
                    preg_replace('/[\r\n]+/', '', $str),
804
                    ENT_QUOTES,
805
                    'UTF-8'
806
                ), "<br>\n";
807
                break;
808
            case 'echo':
809
            default:
810
                //Normalize line breaks
811
                $str = preg_replace('/\r\n|\r/ms', "\n", $str);
812
                echo gmdate('Y-m-d H:i:s'),
813
                "\t",
814
                    //Trim trailing space
815
                trim(
816
                //Indent for readability, except for trailing break
817
                    str_replace(
818
                        "\n",
819
                        "\n                   \t                  ",
820
                        trim($str)
821
                    )
822
                ),
823
                "\n";
824
        }
825
    }
826
827
    /**
828
     * Sets message type to HTML or plain.
829
     *
830
     * @param bool $isHtml True for HTML mode
831
     */
832
    public function isHTML($isHtml = true)
833
    {
834
        if ($isHtml) {
835
            $this->ContentType = 'text/html';
836
        } else {
837
            $this->ContentType = 'text/plain';
838
        }
839
    }
840
841
    /**
842
     * Send messages using SMTP.
843
     */
844
    public function isSMTP()
845
    {
846
        $this->Mailer = 'smtp';
847
    }
848
849
    /**
850
     * Send messages using PHP's mail() function.
851
     */
852
    public function isMail()
853
    {
854
        $this->Mailer = 'mail';
855
    }
856
857
    /**
858
     * Send messages using $Sendmail.
859
     */
860
    public function isSendmail()
861
    {
862
        $ini_sendmail_path = ini_get('sendmail_path');
863
864
        if (!stristr($ini_sendmail_path, 'sendmail')) {
865
            $this->Sendmail = '/usr/sbin/sendmail';
866
        } else {
867
            $this->Sendmail = $ini_sendmail_path;
868
        }
869
        $this->Mailer = 'sendmail';
870
    }
871
872
    /**
873
     * Send messages using qmail.
874
     */
875
    public function isQmail()
876
    {
877
        $ini_sendmail_path = ini_get('sendmail_path');
878
879
        if (!stristr($ini_sendmail_path, 'qmail')) {
880
            $this->Sendmail = '/var/qmail/bin/qmail-inject';
881
        } else {
882
            $this->Sendmail = $ini_sendmail_path;
883
        }
884
        $this->Mailer = 'qmail';
885
    }
886
887
    /**
888
     * Add a "To" address.
889
     *
890
     * @param string $address The email address to send to
891
     * @param string $name
892
     *
893
     * @return bool true on success, false if address already used or invalid in some way
894
     */
895
    public function addAddress($address, $name = '')
896
    {
897
        return $this->addOrEnqueueAnAddress('to', $address, $name);
898
    }
899
900
    /**
901
     * Add a "CC" address.
902
     *
903
     * @param string $address The email address to send to
904
     * @param string $name
905
     *
906
     * @return bool true on success, false if address already used or invalid in some way
907
     */
908
    public function addCC($address, $name = '')
909
    {
910
        return $this->addOrEnqueueAnAddress('cc', $address, $name);
911
    }
912
913
    /**
914
     * Add a "BCC" address.
915
     *
916
     * @param string $address The email address to send to
917
     * @param string $name
918
     *
919
     * @return bool true on success, false if address already used or invalid in some way
920
     */
921
    public function addBCC($address, $name = '')
922
    {
923
        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
924
    }
925
926
    /**
927
     * Add a "Reply-To" address.
928
     *
929
     * @param string $address The email address to reply to
930
     * @param string $name
931
     *
932
     * @return bool true on success, false if address already used or invalid in some way
933
     */
934
    public function addReplyTo($address, $name = '')
935
    {
936
        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
937
    }
938
939
    /**
940
     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
941
     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
942
     * be modified after calling this function), addition of such addresses is delayed until send().
943
     * Addresses that have been added already return false, but do not throw exceptions.
944
     *
945
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
946
     * @param string $address The email address to send, resp. to reply to
947
     * @param string $name
948
     *
949
     * @throws Exception
950
     *
951
     * @return bool true on success, false if address already used or invalid in some way
952
     */
953
    protected function addOrEnqueueAnAddress($kind, $address, $name)
954
    {
955
        $address = trim($address);
956
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
957
        $pos = strrpos($address, '@');
958
        if (false === $pos) {
959
            // At-sign is missing.
960
            $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address";
961
            $this->setError($error_message);
962
            $this->edebug($error_message);
963
            if ($this->exceptions) {
964
                throw new Exception($error_message);
965
            }
966
967
            return false;
968
        }
969
        $params = [$kind, $address, $name];
970
        // Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
971
        if ($this->has8bitChars(substr($address, ++$pos)) and $this->idnSupported()) {
972
            if ('Reply-To' != $kind) {
973
                if (!array_key_exists($address, $this->RecipientsQueue)) {
974
                    $this->RecipientsQueue[$address] = $params;
975
976
                    return true;
977
                }
978
            } else {
979
                if (!array_key_exists($address, $this->ReplyToQueue)) {
980
                    $this->ReplyToQueue[$address] = $params;
981
982
                    return true;
983
                }
984
            }
985
986
            return false;
987
        }
988
989
        // Immediately add standard addresses without IDN.
990
        return call_user_func_array([$this, 'addAnAddress'], $params);
991
    }
992
993
    /**
994
     * Add an address to one of the recipient arrays or to the ReplyTo array.
995
     * Addresses that have been added already return false, but do not throw exceptions.
996
     *
997
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
998
     * @param string $address The email address to send, resp. to reply to
999
     * @param string $name
1000
     *
1001
     * @throws Exception
1002
     *
1003
     * @return bool true on success, false if address already used or invalid in some way
1004
     */
1005
    protected function addAnAddress($kind, $address, $name = '')
1006
    {
1007
        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1008
            $error_message = $this->lang('Invalid recipient kind: ') . $kind;
1009
            $this->setError($error_message);
1010
            $this->edebug($error_message);
1011
            if ($this->exceptions) {
1012
                throw new Exception($error_message);
1013
            }
1014
1015
            return false;
1016
        }
1017
        if (!static::validateAddress($address)) {
1018
            $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address";
1019
            $this->setError($error_message);
1020
            $this->edebug($error_message);
1021
            if ($this->exceptions) {
1022
                throw new Exception($error_message);
1023
            }
1024
1025
            return false;
1026
        }
1027
        if ('Reply-To' != $kind) {
1028
            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
1029
                array_push($this->$kind, [$address, $name]);
1030
                $this->all_recipients[strtolower($address)] = true;
1031
1032
                return true;
1033
            }
1034
        } else {
1035
            if (!array_key_exists(strtolower($address), $this->ReplyTo)) {
1036
                $this->ReplyTo[strtolower($address)] = [$address, $name];
1037
1038
                return true;
1039
            }
1040
        }
1041
1042
        return false;
1043
    }
1044
1045
    /**
1046
     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1047
     * of the form "display name <address>" into an array of name/address pairs.
1048
     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1049
     * Note that quotes in the name part are removed.
1050
     *
1051
     * @see    http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1052
     *
1053
     * @param string $addrstr The address list string
1054
     * @param bool   $useimap Whether to use the IMAP extension to parse the list
1055
     *
1056
     * @return array
1057
     */
1058
    public static function parseAddresses($addrstr, $useimap = true)
1059
    {
1060
        $addresses = [];
1061
        if ($useimap and function_exists('imap_rfc822_parse_adrlist')) {
1062
            //Use this built-in parser if it's available
1063
            $list = imap_rfc822_parse_adrlist($addrstr, '');
1064
            foreach ($list as $address) {
1065
                if ('.SYNTAX-ERROR.' != $address->host) {
1066
                    if (static::validateAddress($address->mailbox . '@' . $address->host)) {
1067
                        $addresses[] = [
1068
                            'name' => (property_exists($address, 'personal') ? $address->personal : ''),
1069
                            'address' => $address->mailbox . '@' . $address->host,
1070
                        ];
1071
                    }
1072
                }
1073
            }
1074
        } else {
1075
            //Use this simpler parser
1076
            $list = explode(',', $addrstr);
1077
            foreach ($list as $address) {
1078
                $address = trim($address);
1079
                //Is there a separate name part?
1080
                if (strpos($address, '<') === false) {
1081
                    //No separate name, just use the whole thing
1082
                    if (static::validateAddress($address)) {
1083
                        $addresses[] = [
1084
                            'name' => '',
1085
                            'address' => $address,
1086
                        ];
1087
                    }
1088
                } else {
1089
                    list($name, $email) = explode('<', $address);
1090
                    $email = trim(str_replace('>', '', $email));
1091
                    if (static::validateAddress($email)) {
1092
                        $addresses[] = [
1093
                            'name' => trim(str_replace(['"', "'"], '', $name)),
1094
                            'address' => $email,
1095
                        ];
1096
                    }
1097
                }
1098
            }
1099
        }
1100
1101
        return $addresses;
1102
    }
1103
1104
    /**
1105
     * Set the From and FromName properties.
1106
     *
1107
     * @param string $address
1108
     * @param string $name
1109
     * @param bool   $auto    Whether to also set the Sender address, defaults to true
1110
     *
1111
     * @throws Exception
1112
     *
1113
     * @return bool
1114
     */
1115
    public function setFrom($address, $name = '', $auto = true)
1116
    {
1117
        $address = trim($address);
1118
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1119
        // Don't validate now addresses with IDN. Will be done in send().
1120
        $pos = strrpos($address, '@');
1121
        if (false === $pos or
1122
            (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and
1123
            !static::validateAddress($address)) {
1124
            $error_message = $this->lang('invalid_address') . " (setFrom) $address";
1125
            $this->setError($error_message);
1126
            $this->edebug($error_message);
1127
            if ($this->exceptions) {
1128
                throw new Exception($error_message);
1129
            }
1130
1131
            return false;
1132
        }
1133
        $this->From = $address;
1134
        $this->FromName = $name;
1135
        if ($auto) {
1136
            if (empty($this->Sender)) {
1137
                $this->Sender = $address;
1138
            }
1139
        }
1140
1141
        return true;
1142
    }
1143
1144
    /**
1145
     * Return the Message-ID header of the last email.
1146
     * Technically this is the value from the last time the headers were created,
1147
     * but it's also the message ID of the last sent message except in
1148
     * pathological cases.
1149
     *
1150
     * @return string
1151
     */
1152
    public function getLastMessageID()
1153
    {
1154
        return $this->lastMessageID;
1155
    }
1156
1157
    /**
1158
     * Check that a string looks like an email address.
1159
     * Validation patterns supported:
1160
     * * `auto` Pick best pattern automatically;
1161
     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
1162
     * * `pcre` Use old PCRE implementation;
1163
     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
1164
     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
1165
     * * `noregex` Don't use a regex: super fast, really dumb.
1166
     * Alternatively you may pass in a callable to inject your own validator, for example:
1167
     *
1168
     * ```php
1169
     * PHPMailer::validateAddress('[email protected]', function($address) {
1170
     *     return (strpos($address, '@') !== false);
1171
     * });
1172
     * ```
1173
     *
1174
     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
1175
     *
1176
     * @param string          $address       The email address to check
1177
     * @param string|callable $patternselect Which pattern to use
1178
     *
1179
     * @return bool
1180
     */
1181
    public static function validateAddress($address, $patternselect = null)
1182
    {
1183
        if (null === $patternselect) {
1184
            $patternselect = static::$validator;
1185
        }
1186
        if (is_callable($patternselect)) {
1187
            return call_user_func($patternselect, $address);
1188
        }
1189
        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
1190
        if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) {
1191
            return false;
1192
        }
1193
        switch ($patternselect) {
1194
            case 'pcre': //Kept for BC
1195
            case 'pcre8':
1196
                /*
1197
                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
1198
                 * is based.
1199
                 * In addition to the addresses allowed by filter_var, also permits:
1200
                 *  * dotless domains: `a@b`
1201
                 *  * comments: `1234 @ local(blah) .machine .example`
1202
                 *  * quoted elements: `'"test blah"@example.org'`
1203
                 *  * numeric TLDs: `[email protected]`
1204
                 *  * unbracketed IPv4 literals: `[email protected]`
1205
                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'
1206
                 * Not all of these will necessarily work for sending!
1207
                 *
1208
                 * @see       http://squiloople.com/2009/12/20/email-address-validation/
1209
                 * @copyright 2009-2010 Michael Rushton
1210
                 * Feel free to use and redistribute this code. But please keep this copyright notice.
1211
                 */
1212
                return (bool) preg_match(
1213
                    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
1214
                    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
1215
                    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
1216
                    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
1217
                    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
1218
                    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
1219
                    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
1220
                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
1221
                    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
1222
                    $address
1223
                );
1224
            case 'html5':
1225
                /*
1226
                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
1227
                 *
1228
                 * @see http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email)
1229
                 */
1230
                return (bool) preg_match(
1231
                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
1232
                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
1233
                    $address
1234
                );
1235
            case 'php':
1236
            default:
1237
                return (bool) filter_var($address, FILTER_VALIDATE_EMAIL);
1238
        }
1239
    }
1240
1241
    /**
1242
     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
1243
     * `intl` and `mbstring` PHP extensions.
1244
     *
1245
     * @return bool `true` if required functions for IDN support are present
1246
     */
1247
    public function idnSupported()
1248
    {
1249
        return function_exists('idn_to_ascii') and function_exists('mb_convert_encoding');
1250
    }
1251
1252
    /**
1253
     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
1254
     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
1255
     * This function silently returns unmodified address if:
1256
     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
1257
     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
1258
     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).
1259
     *
1260
     * @see    PHPMailer::$CharSet
1261
     *
1262
     * @param string $address The email address to convert
1263
     *
1264
     * @return string The encoded address in ASCII form
1265
     */
1266
    public function punyencodeAddress($address)
1267
    {
1268
        // Verify we have required functions, CharSet, and at-sign.
1269
        $pos = strrpos($address, '@');
1270
        if ($this->idnSupported() and
1271
            !empty($this->CharSet) and
1272
            false !== $pos
1273
        ) {
1274
            $domain = substr($address, ++$pos);
1275
            // Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
1276
            if ($this->has8bitChars($domain) and @mb_check_encoding($domain, $this->CharSet)) {
1277
                $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet);
1278
                //Ignore IDE complaints about this line - method signature changed in PHP 5.4
1279
                $errorcode = 0;
1280
                $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46);
1281
                if (false !== $punycode) {
1282
                    return substr($address, 0, $pos) . $punycode;
1283
                }
1284
            }
1285
        }
1286
1287
        return $address;
1288
    }
1289
1290
    /**
1291
     * Create a message and send it.
1292
     * Uses the sending method specified by $Mailer.
1293
     *
1294
     * @throws Exception
1295
     *
1296
     * @return bool false on error - See the ErrorInfo property for details of the error
1297
     */
1298
    public function send()
1299
    {
1300
        try {
1301
            if (!$this->preSend()) {
1302
                return false;
1303
            }
1304
1305
            return $this->postSend();
1306
        } catch (Exception $exc) {
1307
            $this->mailHeader = '';
1308
            $this->setError($exc->getMessage());
1309
            if ($this->exceptions) {
1310
                throw $exc;
1311
            }
1312
1313
            return false;
1314
        }
1315
    }
1316
1317
    /**
1318
     * Prepare a message for sending.
1319
     *
1320
     * @throws Exception
1321
     *
1322
     * @return bool
1323
     */
1324
    public function preSend()
0 ignored issues
show
Complexity introduced by
This operation has 96832 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
1325
    {
1326
        if ('smtp' == $this->Mailer or
1327
            ('mail' == $this->Mailer and stripos(PHP_OS, 'WIN') === 0)
1328
        ) {
1329
            //SMTP mandates RFC-compliant line endings
1330
            //and it's also used with mail() on Windows
1331
            static::setLE("\r\n");
1332
        } else {
1333
            //Maintain backward compatibility with legacy Linux command line mailers
1334
            static::setLE(PHP_EOL);
1335
        }
1336
        //Check for buggy PHP versions that add a header with an incorrect line break
1337
        if (ini_get('mail.add_x_header') == 1
1338
            and 'mail' == $this->Mailer
1339
            and stripos(PHP_OS, 'WIN') === 0
1340
            and ((version_compare(PHP_VERSION, '7.0.0', '>=')
1341
                    and version_compare(PHP_VERSION, '7.0.17', '<'))
1342
                or (version_compare(PHP_VERSION, '7.1.0', '>=')
1343
                    and version_compare(PHP_VERSION, '7.1.3', '<')))
1344
        ) {
1345
            trigger_error(
1346
                'Your version of PHP is affected by a bug that may result in corrupted messages.' .
1347
                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
1348
                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
1349
                E_USER_WARNING
1350
            );
1351
        }
1352
1353
        try {
1354
            $this->error_count = 0; // Reset errors
1355
            $this->mailHeader = '';
1356
1357
            // Dequeue recipient and Reply-To addresses with IDN
1358
            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
1359
                $params[1] = $this->punyencodeAddress($params[1]);
1360
                call_user_func_array([$this, 'addAnAddress'], $params);
1361
            }
1362
            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
1363
                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
1364
            }
1365
1366
            // Validate From, Sender, and ConfirmReadingTo addresses
1367
            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
1368
                $this->$address_kind = trim($this->$address_kind);
1369
                if (empty($this->$address_kind)) {
1370
                    continue;
1371
                }
1372
                $this->$address_kind = $this->punyencodeAddress($this->$address_kind);
1373
                if (!static::validateAddress($this->$address_kind)) {
1374
                    $error_message = $this->lang('invalid_address') . ' (punyEncode) ' . $this->$address_kind;
1375
                    $this->setError($error_message);
1376
                    $this->edebug($error_message);
1377
                    if ($this->exceptions) {
1378
                        throw new Exception($error_message);
1379
                    }
1380
1381
                    return false;
1382
                }
1383
            }
1384
1385
            // Set whether the message is multipart/alternative
1386
            if ($this->alternativeExists()) {
1387
                $this->ContentType = 'multipart/alternative';
1388
            }
1389
1390
            $this->setMessageType();
1391
            // Refuse to send an empty message unless we are specifically allowing it
1392
            if (!$this->AllowEmpty and empty($this->Body)) {
1393
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
1394
            }
1395
1396
            //Trim subject consistently
1397
            $this->Subject = trim($this->Subject);
1398
            // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
1399
            $this->MIMEHeader = '';
1400
            $this->MIMEBody = $this->createBody();
1401
            // createBody may have added some headers, so retain them
1402
            $tempheaders = $this->MIMEHeader;
1403
            $this->MIMEHeader = $this->createHeader();
1404
            $this->MIMEHeader .= $tempheaders;
1405
1406
            // To capture the complete message when using mail(), create
1407
            // an extra header list which createHeader() doesn't fold in
1408
            if ('mail' == $this->Mailer) {
1409
                if (count($this->to) > 0) {
1410
                    $this->mailHeader .= $this->addrAppend('To', $this->to);
1411
                } else {
1412
                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
1413
                }
1414
                $this->mailHeader .= $this->headerLine(
1415
                    'Subject',
1416
                    $this->encodeHeader($this->secureHeader($this->Subject))
1417
                );
1418
            }
1419
1420
            // Sign with DKIM if enabled
1421
            if (!empty($this->DKIM_domain)
1422
                and !empty($this->DKIM_selector)
1423
                and (!empty($this->DKIM_private_string)
1424
                    or (!empty($this->DKIM_private) and file_exists($this->DKIM_private))
1425
                )
1426
            ) {
1427
                $header_dkim = $this->DKIM_Add(
1428
                    $this->MIMEHeader . $this->mailHeader,
1429
                    $this->encodeHeader($this->secureHeader($this->Subject)),
1430
                    $this->MIMEBody
1431
                );
1432
                $this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . static::$LE .
1433
                    static::normalizeBreaks($header_dkim) . static::$LE;
1434
            }
1435
1436
            return true;
1437
        } catch (Exception $exc) {
1438
            $this->setError($exc->getMessage());
1439
            if ($this->exceptions) {
1440
                throw $exc;
1441
            }
1442
1443
            return false;
1444
        }
1445
    }
1446
1447
    /**
1448
     * Actually send a message via the selected mechanism.
1449
     *
1450
     * @throws Exception
1451
     *
1452
     * @return bool
1453
     */
1454
    public function postSend()
1455
    {
1456
        try {
1457
            // Choose the mailer and send through it
1458
            switch ($this->Mailer) {
1459
                case 'sendmail':
1460
                case 'qmail':
1461
                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
1462
                case 'smtp':
1463
                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
1464
                case 'mail':
1465
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1466
                default:
1467
                    $sendMethod = $this->Mailer . 'Send';
1468
                    if (method_exists($this, $sendMethod)) {
1469
                        return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody);
1470
                    }
1471
1472
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1473
            }
1474
        } catch (Exception $exc) {
1475
            $this->setError($exc->getMessage());
1476
            $this->edebug($exc->getMessage());
1477
            if ($this->exceptions) {
1478
                throw $exc;
1479
            }
1480
        }
1481
1482
        return false;
1483
    }
1484
1485
    /**
1486
     * Send mail using the $Sendmail program.
1487
     *
1488
     * @see    PHPMailer::$Sendmail
1489
     *
1490
     * @param string $header The message headers
1491
     * @param string $body   The message body
1492
     *
1493
     * @throws Exception
1494
     *
1495
     * @return bool
1496
     */
1497
    protected function sendmailSend($header, $body)
1498
    {
1499
        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1500
        if (!empty($this->Sender) and self::isShellSafe($this->Sender)) {
1501
            if ('qmail' == $this->Mailer) {
1502
                $sendmailFmt = '%s -f%s';
1503
            } else {
1504
                $sendmailFmt = '%s -oi -f%s -t';
1505
            }
1506
        } else {
1507
            if ('qmail' == $this->Mailer) {
1508
                $sendmailFmt = '%s';
1509
            } else {
1510
                $sendmailFmt = '%s -oi -t';
1511
            }
1512
        }
1513
1514
        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
1515
1516
        if ($this->SingleTo) {
1517
            foreach ($this->SingleToArray as $toAddr) {
1518
                $mail = @popen($sendmail, 'w');
1519
                if (!$mail) {
1520
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1521
                }
1522
                fwrite($mail, 'To: ' . $toAddr . "\n");
1523
                fwrite($mail, $header);
1524
                fwrite($mail, $body);
1525
                $result = pclose($mail);
1526
                $this->doCallback(
1527
                    ($result == 0),
1528
                    [$toAddr],
1529
                    $this->cc,
1530
                    $this->bcc,
1531
                    $this->Subject,
1532
                    $body,
1533
                    $this->From
1534
                );
1535
                if (0 !== $result) {
1536
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1537
                }
1538
            }
1539
        } else {
1540
            $mail = @popen($sendmail, 'w');
1541
            if (!$mail) {
1542
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1543
            }
1544
            fwrite($mail, $header);
1545
            fwrite($mail, $body);
1546
            $result = pclose($mail);
1547
            $this->doCallback(
1548
                ($result == 0),
1549
                $this->to,
1550
                $this->cc,
1551
                $this->bcc,
1552
                $this->Subject,
1553
                $body,
1554
                $this->From
1555
            );
1556
            if (0 !== $result) {
1557
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1558
            }
1559
        }
1560
1561
        return true;
1562
    }
1563
1564
    /**
1565
     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
1566
     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
1567
     *
1568
     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
1569
     *
1570
     * @param string $string The string to be validated
1571
     *
1572
     * @return bool
1573
     */
1574
    protected static function isShellSafe($string)
1575
    {
1576
        // Future-proof
1577
        if (escapeshellcmd($string) !== $string
1578
            or !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
1579
        ) {
1580
            return false;
1581
        }
1582
1583
        $length = strlen($string);
1584
1585
        for ($i = 0; $i < $length; ++$i) {
1586
            $c = $string[$i];
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $c. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
1587
1588
            // All other characters have a special meaning in at least one common shell, including = and +.
1589
            // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
1590
            // Note that this does permit non-Latin alphanumeric characters based on the current locale.
1591
            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
1592
                return false;
1593
            }
1594
        }
1595
1596
        return true;
1597
    }
1598
1599
    /**
1600
     * Send mail using the PHP mail() function.
1601
     *
1602
     * @see    http://www.php.net/manual/en/book.mail.php
1603
     *
1604
     * @param string $header The message headers
1605
     * @param string $body   The message body
1606
     *
1607
     * @throws Exception
1608
     *
1609
     * @return bool
1610
     */
1611
    protected function mailSend($header, $body)
1612
    {
1613
        $toArr = [];
1614
        foreach ($this->to as $toaddr) {
1615
            $toArr[] = $this->addrFormat($toaddr);
1616
        }
1617
        $to = implode(', ', $toArr);
1618
1619
        $params = null;
1620
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1621
        if (!empty($this->Sender) and static::validateAddress($this->Sender)) {
1622
            //A space after `-f` is optional, but there is a long history of its presence
1623
            //causing problems, so we don't use one
1624
            //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1625
            //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
1626
            //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
1627
            //Example problem: https://www.drupal.org/node/1057954
1628
            // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1629
            if (self::isShellSafe($this->Sender)) {
1630
                $params = sprintf('-f%s', $this->Sender);
1631
            }
1632
        }
1633
        if (!empty($this->Sender) and static::validateAddress($this->Sender)) {
1634
            $old_from = ini_get('sendmail_from');
1635
            ini_set('sendmail_from', $this->Sender);
1636
        }
1637
        $result = false;
1638
        if ($this->SingleTo and count($toArr) > 1) {
1639
            foreach ($toArr as $toAddr) {
1640
                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
1641
                $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From);
1642
            }
1643
        } else {
1644
            $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
1645
            $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From);
1646
        }
1647
        if (isset($old_from)) {
1648
            ini_set('sendmail_from', $old_from);
1649
        }
1650
        if (!$result) {
1651
            throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
1652
        }
1653
1654
        return true;
1655
    }
1656
1657
    /**
1658
     * Get an instance to use for SMTP operations.
1659
     * Override this function to load your own SMTP implementation,
1660
     * or set one with setSMTPInstance.
1661
     *
1662
     * @return SMTP
1663
     */
1664
    public function getSMTPInstance()
1665
    {
1666
        if (!is_object($this->smtp)) {
1667
            $this->smtp = new SMTP();
1668
        }
1669
1670
        return $this->smtp;
1671
    }
1672
1673
    /**
1674
     * Provide an instance to use for SMTP operations.
1675
     *
1676
     * @param SMTP $smtp
1677
     *
1678
     * @return SMTP
1679
     */
1680
    public function setSMTPInstance(SMTP $smtp)
1681
    {
1682
        $this->smtp = $smtp;
1683
1684
        return $this->smtp;
1685
    }
1686
1687
    /**
1688
     * Send mail via SMTP.
1689
     * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
1690
     *
1691
     * @see PHPMailer::setSMTPInstance() to use a different class.
1692
     *
1693
     * @uses \PHPMailer\PHPMailer\SMTP
1694
     *
1695
     * @param string $header The message headers
1696
     * @param string $body   The message body
1697
     *
1698
     * @throws Exception
1699
     *
1700
     * @return bool
1701
     */
1702
    protected function smtpSend($header, $body)
0 ignored issues
show
Complexity introduced by
This operation has 576 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
1703
    {
1704
        $bad_rcpt = [];
1705
        if (!$this->smtpConnect($this->SMTPOptions)) {
1706
            throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
1707
        }
1708
        //Sender already validated in preSend()
1709
        if ('' == $this->Sender) {
1710
            $smtp_from = $this->From;
1711
        } else {
1712
            $smtp_from = $this->Sender;
1713
        }
1714
        if (!$this->smtp->mail($smtp_from)) {
1715
            $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
1716
            throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
1717
        }
1718
1719
        // Attempt to send to all recipients
1720
        foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
1721
            foreach ($togroup as $to) {
1722
                if (!$this->smtp->recipient($to[0])) {
1723
                    $error = $this->smtp->getError();
1724
                    $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
1725
                    $isSent = false;
1726
                } else {
1727
                    $isSent = true;
1728
                }
1729
                $this->doCallback($isSent, [$to[0]], [], [], $this->Subject, $body, $this->From);
1730
            }
1731
        }
1732
1733
        // Only send the DATA command if we have viable recipients
1734
        if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) {
1735
            throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
1736
        }
1737
        if ($this->SMTPKeepAlive) {
1738
            $this->smtp->reset();
1739
        } else {
1740
            $this->smtp->quit();
1741
            $this->smtp->close();
1742
        }
1743
        //Create error message for any bad addresses
1744
        if (count($bad_rcpt) > 0) {
1745
            $errstr = '';
1746
            foreach ($bad_rcpt as $bad) {
1747
                $errstr .= $bad['to'] . ': ' . $bad['error'];
1748
            }
1749
            throw new Exception(
1750
                $this->lang('recipients_failed') . $errstr,
1751
                self::STOP_CONTINUE
1752
            );
1753
        }
1754
1755
        return true;
1756
    }
1757
1758
    /**
1759
     * Initiate a connection to an SMTP server.
1760
     * Returns false if the operation failed.
1761
     *
1762
     * @param array $options An array of options compatible with stream_context_create()
1763
     *
1764
     * @throws Exception
1765
     *
1766
     * @uses \PHPMailer\PHPMailer\SMTP
1767
     *
1768
     * @return bool
1769
     */
1770
    public function smtpConnect($options = null)
0 ignored issues
show
Complexity introduced by
This operation has 529944 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
1771
    {
1772
        if (null === $this->smtp) {
1773
            $this->smtp = $this->getSMTPInstance();
1774
        }
1775
1776
        //If no options are provided, use whatever is set in the instance
1777
        if (null === $options) {
1778
            $options = $this->SMTPOptions;
1779
        }
1780
1781
        // Already connected?
1782
        if ($this->smtp->connected()) {
1783
            return true;
1784
        }
1785
1786
        $this->smtp->setTimeout($this->Timeout);
1787
        $this->smtp->setDebugLevel($this->SMTPDebug);
1788
        $this->smtp->setDebugOutput($this->Debugoutput);
1789
        $this->smtp->setVerp($this->do_verp);
1790
        $hosts = explode(';', $this->Host);
1791
        $lastexception = null;
1792
1793
        foreach ($hosts as $hostentry) {
1794
            $hostinfo = [];
1795
            if (!preg_match(
1796
                '/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*|\[[a-fA-F0-9:]+\]):?([0-9]*)$/',
1797
                trim($hostentry),
1798
                $hostinfo
1799
            )) {
1800
                static::edebug($this->lang('connect_host') . ' ' . $hostentry);
1801
                // Not a valid host entry
1802
                continue;
1803
            }
1804
            // $hostinfo[2]: optional ssl or tls prefix
1805
            // $hostinfo[3]: the hostname
1806
            // $hostinfo[4]: optional port number
1807
            // The host string prefix can temporarily override the current setting for SMTPSecure
1808
            // If it's not specified, the default value is used
1809
1810
            //Check the host name is a valid name or IP address before trying to use it
1811
            if (!static::isValidHost($hostinfo[3])) {
1812
                static::edebug($this->lang('connect_host') . ' ' . $hostentry);
1813
                continue;
1814
            }
1815
            $prefix = '';
1816
            $secure = $this->SMTPSecure;
1817
            $tls = ('tls' == $this->SMTPSecure);
1818
            if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) {
1819
                $prefix = 'ssl://';
1820
                $tls = false; // Can't have SSL and TLS at the same time
1821
                $secure = 'ssl';
1822
            } elseif ('tls' == $hostinfo[2]) {
1823
                $tls = true;
1824
                // tls doesn't use a prefix
1825
                $secure = 'tls';
1826
            }
1827
            //Do we need the OpenSSL extension?
1828
            $sslext = defined('OPENSSL_ALGO_SHA256');
1829
            if ('tls' === $secure or 'ssl' === $secure) {
1830
                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
1831
                if (!$sslext) {
1832
                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
1833
                }
1834
            }
1835
            $host = $hostinfo[3];
1836
            $port = $this->Port;
1837
            $tport = (int) $hostinfo[4];
1838
            if ($tport > 0 and $tport < 65536) {
1839
                $port = $tport;
1840
            }
1841
            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
1842
                try {
1843
                    if ($this->Helo) {
1844
                        $hello = $this->Helo;
1845
                    } else {
1846
                        $hello = $this->serverHostname();
1847
                    }
1848
                    $this->smtp->hello($hello);
1849
                    //Automatically enable TLS encryption if:
1850
                    // * it's not disabled
1851
                    // * we have openssl extension
1852
                    // * we are not already using SSL
1853
                    // * the server offers STARTTLS
1854
                    if ($this->SMTPAutoTLS and $sslext and $secure != 'ssl' and $this->smtp->getServerExt('STARTTLS')) {
1855
                        $tls = true;
1856
                    }
1857
                    if ($tls) {
1858
                        if (!$this->smtp->startTLS()) {
1859
                            throw new Exception($this->lang('connect_host'));
1860
                        }
1861
                        // We must resend EHLO after TLS negotiation
1862
                        $this->smtp->hello($hello);
1863
                    }
1864
                    if ($this->SMTPAuth) {
1865
                        if (!$this->smtp->authenticate(
1866
                            $this->Username,
1867
                            $this->Password,
1868
                            $this->AuthType,
1869
                            $this->oauth
1870
                        )
1871
                        ) {
1872
                            throw new Exception($this->lang('authenticate'));
1873
                        }
1874
                    }
1875
1876
                    return true;
1877
                } catch (Exception $exc) {
1878
                    $lastexception = $exc;
1879
                    $this->edebug($exc->getMessage());
1880
                    // We must have connected, but then failed TLS or Auth, so close connection nicely
1881
                    $this->smtp->quit();
1882
                }
1883
            }
1884
        }
1885
        // If we get here, all connection attempts have failed, so close connection hard
1886
        $this->smtp->close();
1887
        // As we've caught all exceptions, just report whatever the last one was
1888
        if ($this->exceptions and null !== $lastexception) {
1889
            throw $lastexception;
1890
        }
1891
1892
        return false;
1893
    }
1894
1895
    /**
1896
     * Close the active SMTP session if one exists.
1897
     */
1898
    public function smtpClose()
1899
    {
1900
        if (null !== $this->smtp) {
1901
            if ($this->smtp->connected()) {
1902
                $this->smtp->quit();
1903
                $this->smtp->close();
1904
            }
1905
        }
1906
    }
1907
1908
    /**
1909
     * Set the language for error messages.
1910
     * Returns false if it cannot load the language file.
1911
     * The default language is English.
1912
     *
1913
     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
1914
     * @param string $lang_path Path to the language file directory, with trailing separator (slash)
1915
     *
1916
     * @return bool
1917
     */
1918
    public function setLanguage($langcode = 'en', $lang_path = '')
1919
    {
1920
        // Backwards compatibility for renamed language codes
1921
        $renamed_langcodes = [
1922
            'br' => 'pt_br',
1923
            'cz' => 'cs',
1924
            'dk' => 'da',
1925
            'no' => 'nb',
1926
            'se' => 'sv',
1927
            'sr' => 'rs',
1928
        ];
1929
1930
        if (isset($renamed_langcodes[$langcode])) {
1931
            $langcode = $renamed_langcodes[$langcode];
1932
        }
1933
1934
        // Define full set of translatable strings in English
1935
        $PHPMAILER_LANG = [
1936
            'authenticate' => 'SMTP Error: Could not authenticate.',
1937
            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
1938
            'data_not_accepted' => 'SMTP Error: data not accepted.',
1939
            'empty_message' => 'Message body empty',
1940
            'encoding' => 'Unknown encoding: ',
1941
            'execute' => 'Could not execute: ',
1942
            'file_access' => 'Could not access file: ',
1943
            'file_open' => 'File Error: Could not open file: ',
1944
            'from_failed' => 'The following From address failed: ',
1945
            'instantiate' => 'Could not instantiate mail function.',
1946
            'invalid_address' => 'Invalid address: ',
1947
            'mailer_not_supported' => ' mailer is not supported.',
1948
            'provide_address' => 'You must provide at least one recipient email address.',
1949
            'recipients_failed' => 'SMTP Error: The following recipients failed: ',
1950
            'signing' => 'Signing Error: ',
1951
            'smtp_connect_failed' => 'SMTP connect() failed.',
1952
            'smtp_error' => 'SMTP server error: ',
1953
            'variable_set' => 'Cannot set or reset variable: ',
1954
            'extension_missing' => 'Extension missing: ',
1955
        ];
1956
        if (empty($lang_path)) {
1957
            // Calculate an absolute path so it can work if CWD is not here
1958
            $lang_path = __DIR__ . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
1959
        }
1960
        //Validate $langcode
1961
        if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) {
1962
            $langcode = 'en';
1963
        }
1964
        $foundlang = true;
1965
        $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
1966
        // There is no English translation file
1967
        if ('en' != $langcode) {
1968
            // Make sure language file path is readable
1969
            if (!file_exists($lang_file)) {
1970
                $foundlang = false;
1971
            } else {
1972
                // Overwrite language-specific strings.
1973
                // This way we'll never have missing translation keys.
1974
                $foundlang = include $lang_file;
1975
            }
1976
        }
1977
        $this->language = $PHPMAILER_LANG;
1978
1979
        return (bool) $foundlang; // Returns false if language not found
1980
    }
1981
1982
    /**
1983
     * Get the array of strings for the current language.
1984
     *
1985
     * @return array
1986
     */
1987
    public function getTranslations()
1988
    {
1989
        return $this->language;
1990
    }
1991
1992
    /**
1993
     * Create recipient headers.
1994
     *
1995
     * @param string $type
1996
     * @param array  $addr An array of recipients,
1997
     *                     where each recipient is a 2-element indexed array with element 0 containing an address
1998
     *                     and element 1 containing a name, like:
1999
     *                     [['[email protected]', 'Joe User'], ['[email protected]', 'Zoe User']]
2000
     *
2001
     * @return string
2002
     */
2003
    public function addrAppend($type, $addr)
2004
    {
2005
        $addresses = [];
2006
        foreach ($addr as $address) {
2007
            $addresses[] = $this->addrFormat($address);
2008
        }
2009
2010
        return $type . ': ' . implode(', ', $addresses) . static::$LE;
2011
    }
2012
2013
    /**
2014
     * Format an address for use in a message header.
2015
     *
2016
     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
2017
     *                    ['[email protected]', 'Joe User']
2018
     *
2019
     * @return string
2020
     */
2021
    public function addrFormat($addr)
2022
    {
2023
        if (empty($addr[1])) { // No name provided
2024
            return $this->secureHeader($addr[0]);
2025
        }
2026
2027
        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader(
2028
                $addr[0]
2029
            ) . '>';
2030
    }
2031
2032
    /**
2033
     * Word-wrap message.
2034
     * For use with mailers that do not automatically perform wrapping
2035
     * and for quoted-printable encoded messages.
2036
     * Original written by philippe.
2037
     *
2038
     * @param string $message The message to wrap
2039
     * @param int    $length  The line length to wrap to
2040
     * @param bool   $qp_mode Whether to run in Quoted-Printable mode
2041
     *
2042
     * @return string
2043
     */
2044
    public function wrapText($message, $length, $qp_mode = false)
2045
    {
2046
        if ($qp_mode) {
2047
            $soft_break = sprintf(' =%s', static::$LE);
2048
        } else {
2049
            $soft_break = static::$LE;
2050
        }
2051
        // If utf-8 encoding is used, we will need to make sure we don't
2052
        // split multibyte characters when we wrap
2053
        $is_utf8 = (strtolower($this->CharSet) == 'utf-8');
2054
        $lelen = strlen(static::$LE);
2055
        $crlflen = strlen(static::$LE);
2056
2057
        $message = static::normalizeBreaks($message);
2058
        //Remove a trailing line break
2059
        if (substr($message, -$lelen) == static::$LE) {
2060
            $message = substr($message, 0, -$lelen);
2061
        }
2062
2063
        //Split message into lines
2064
        $lines = explode(static::$LE, $message);
2065
        //Message will be rebuilt in here
2066
        $message = '';
2067
        foreach ($lines as $line) {
2068
            $words = explode(' ', $line);
2069
            $buf = '';
2070
            $firstword = true;
2071
            foreach ($words as $word) {
2072
                if ($qp_mode and (strlen($word) > $length)) {
2073
                    $space_left = $length - strlen($buf) - $crlflen;
2074
                    if (!$firstword) {
2075
                        if ($space_left > 20) {
2076
                            $len = $space_left;
2077
                            if ($is_utf8) {
2078
                                $len = $this->utf8CharBoundary($word, $len);
2079
                            } elseif (substr($word, $len - 1, 1) == '=') {
2080
                                --$len;
2081
                            } elseif (substr($word, $len - 2, 1) == '=') {
2082
                                $len -= 2;
2083
                            }
2084
                            $part = substr($word, 0, $len);
2085
                            $word = substr($word, $len);
2086
                            $buf .= ' ' . $part;
2087
                            $message .= $buf . sprintf('=%s', static::$LE);
2088
                        } else {
2089
                            $message .= $buf . $soft_break;
2090
                        }
2091
                        $buf = '';
2092
                    }
2093
                    while (strlen($word) > 0) {
2094
                        if ($length <= 0) {
2095
                            break;
2096
                        }
2097
                        $len = $length;
2098
                        if ($is_utf8) {
2099
                            $len = $this->utf8CharBoundary($word, $len);
2100
                        } elseif (substr($word, $len - 1, 1) == '=') {
2101
                            --$len;
2102
                        } elseif (substr($word, $len - 2, 1) == '=') {
2103
                            $len -= 2;
2104
                        }
2105
                        $part = substr($word, 0, $len);
2106
                        $word = substr($word, $len);
2107
2108
                        if (strlen($word) > 0) {
2109
                            $message .= $part . sprintf('=%s', static::$LE);
2110
                        } else {
2111
                            $buf = $part;
2112
                        }
2113
                    }
2114
                } else {
2115
                    $buf_o = $buf;
2116
                    if (!$firstword) {
2117
                        $buf .= ' ';
2118
                    }
2119
                    $buf .= $word;
2120
2121
                    if (strlen($buf) > $length and $buf_o != '') {
2122
                        $message .= $buf_o . $soft_break;
2123
                        $buf = $word;
2124
                    }
2125
                }
2126
                $firstword = false;
2127
            }
2128
            $message .= $buf . static::$LE;
2129
        }
2130
2131
        return $message;
2132
    }
2133
2134
    /**
2135
     * Find the last character boundary prior to $maxLength in a utf-8
2136
     * quoted-printable encoded string.
2137
     * Original written by Colin Brown.
2138
     *
2139
     * @param string $encodedText utf-8 QP text
2140
     * @param int    $maxLength   Find the last character boundary prior to this length
2141
     *
2142
     * @return int
2143
     */
2144
    public function utf8CharBoundary($encodedText, $maxLength)
2145
    {
2146
        $foundSplitPos = false;
2147
        $lookBack = 3;
2148
        while (!$foundSplitPos) {
2149
            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
2150
            $encodedCharPos = strpos($lastChunk, '=');
2151
            if (false !== $encodedCharPos) {
2152
                // Found start of encoded character byte within $lookBack block.
2153
                // Check the encoded byte value (the 2 chars after the '=')
2154
                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
2155
                $dec = hexdec($hex);
2156
                if ($dec < 128) {
2157
                    // Single byte character.
2158
                    // If the encoded char was found at pos 0, it will fit
2159
                    // otherwise reduce maxLength to start of the encoded char
2160
                    if ($encodedCharPos > 0) {
2161
                        $maxLength -= $lookBack - $encodedCharPos;
2162
                    }
2163
                    $foundSplitPos = true;
2164
                } elseif ($dec >= 192) {
2165
                    // First byte of a multi byte character
2166
                    // Reduce maxLength to split at start of character
2167
                    $maxLength -= $lookBack - $encodedCharPos;
2168
                    $foundSplitPos = true;
2169
                } elseif ($dec < 192) {
2170
                    // Middle byte of a multi byte character, look further back
2171
                    $lookBack += 3;
2172
                }
2173
            } else {
2174
                // No encoded character found
2175
                $foundSplitPos = true;
2176
            }
2177
        }
2178
2179
        return $maxLength;
2180
    }
2181
2182
    /**
2183
     * Apply word wrapping to the message body.
2184
     * Wraps the message body to the number of chars set in the WordWrap property.
2185
     * You should only do this to plain-text bodies as wrapping HTML tags may break them.
2186
     * This is called automatically by createBody(), so you don't need to call it yourself.
2187
     */
2188
    public function setWordWrap()
2189
    {
2190
        if ($this->WordWrap < 1) {
2191
            return;
2192
        }
2193
2194
        switch ($this->message_type) {
2195
            case 'alt':
2196
            case 'alt_inline':
2197
            case 'alt_attach':
2198
            case 'alt_inline_attach':
2199
                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
2200
                break;
2201
            default:
2202
                $this->Body = $this->wrapText($this->Body, $this->WordWrap);
2203
                break;
2204
        }
2205
    }
2206
2207
    /**
2208
     * Assemble message headers.
2209
     *
2210
     * @return string The assembled headers
2211
     */
2212
    public function createHeader()
0 ignored issues
show
Complexity introduced by
This operation has 201600 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
2213
    {
2214
        $result = '';
2215
2216
        $result .= $this->headerLine('Date', '' == $this->MessageDate ? self::rfcDate() : $this->MessageDate);
2217
2218
        // To be created automatically by mail()
2219
        if ($this->SingleTo) {
2220
            if ('mail' != $this->Mailer) {
2221
                foreach ($this->to as $toaddr) {
2222
                    $this->SingleToArray[] = $this->addrFormat($toaddr);
2223
                }
2224
            }
2225
        } else {
2226
            if (count($this->to) > 0) {
2227
                if ('mail' != $this->Mailer) {
2228
                    $result .= $this->addrAppend('To', $this->to);
2229
                }
2230
            } elseif (count($this->cc) == 0) {
2231
                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
2232
            }
2233
        }
2234
2235
        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
2236
2237
        // sendmail and mail() extract Cc from the header before sending
2238
        if (count($this->cc) > 0) {
2239
            $result .= $this->addrAppend('Cc', $this->cc);
2240
        }
2241
2242
        // sendmail and mail() extract Bcc from the header before sending
2243
        if ((
2244
                'sendmail' == $this->Mailer or 'qmail' == $this->Mailer or 'mail' == $this->Mailer
2245
            )
2246
            and count($this->bcc) > 0
2247
        ) {
2248
            $result .= $this->addrAppend('Bcc', $this->bcc);
2249
        }
2250
2251
        if (count($this->ReplyTo) > 0) {
2252
            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
2253
        }
2254
2255
        // mail() sets the subject itself
2256
        if ('mail' != $this->Mailer) {
2257
            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
2258
        }
2259
2260
        // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
2261
        // https://tools.ietf.org/html/rfc5322#section-3.6.4
2262
        if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) {
2263
            $this->lastMessageID = $this->MessageID;
2264
        } else {
2265
            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
2266
        }
2267
        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
2268
        if (null !== $this->Priority) {
2269
            $result .= $this->headerLine('X-Priority', $this->Priority);
2270
        }
2271
        if ('' == $this->XMailer) {
2272
            $result .= $this->headerLine(
2273
                'X-Mailer',
2274
                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
2275
            );
2276
        } else {
2277
            $myXmailer = trim($this->XMailer);
2278
            if ($myXmailer) {
2279
                $result .= $this->headerLine('X-Mailer', $myXmailer);
2280
            }
2281
        }
2282
2283
        if ('' != $this->ConfirmReadingTo) {
2284
            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
2285
        }
2286
2287
        // Add custom headers
2288
        foreach ($this->CustomHeader as $header) {
2289
            $result .= $this->headerLine(
2290
                trim($header[0]),
2291
                $this->encodeHeader(trim($header[1]))
2292
            );
2293
        }
2294
        if (!$this->sign_key_file) {
2295
            $result .= $this->headerLine('MIME-Version', '1.0');
2296
            $result .= $this->getMailMIME();
2297
        }
2298
2299
        return $result;
2300
    }
2301
2302
    /**
2303
     * Get the message MIME type headers.
2304
     *
2305
     * @return string
2306
     */
2307
    public function getMailMIME()
2308
    {
2309
        $result = '';
2310
        $ismultipart = true;
2311
        switch ($this->message_type) {
2312
            case 'inline':
2313
                $result .= $this->headerLine('Content-Type', 'multipart/related;');
2314
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2315
                break;
2316
            case 'attach':
2317
            case 'inline_attach':
2318
            case 'alt_attach':
2319
            case 'alt_inline_attach':
2320
                $result .= $this->headerLine('Content-Type', 'multipart/mixed;');
2321
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2322
                break;
2323
            case 'alt':
2324
            case 'alt_inline':
2325
                $result .= $this->headerLine('Content-Type', 'multipart/alternative;');
2326
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2327
                break;
2328
            default:
2329
                // Catches case 'plain': and case '':
2330
                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
2331
                $ismultipart = false;
2332
                break;
2333
        }
2334
        // RFC1341 part 5 says 7bit is assumed if not specified
2335
        if ('7bit' != $this->Encoding) {
2336
            // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
2337
            if ($ismultipart) {
2338
                if ('8bit' == $this->Encoding) {
2339
                    $result .= $this->headerLine('Content-Transfer-Encoding', '8bit');
2340
                }
2341
                // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
2342
            } else {
2343
                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
2344
            }
2345
        }
2346
2347
        if ('mail' != $this->Mailer) {
2348
            $result .= static::$LE;
2349
        }
2350
2351
        return $result;
2352
    }
2353
2354
    /**
2355
     * Returns the whole MIME message.
2356
     * Includes complete headers and body.
2357
     * Only valid post preSend().
2358
     *
2359
     * @see PHPMailer::preSend()
2360
     *
2361
     * @return string
2362
     */
2363
    public function getSentMIMEMessage()
2364
    {
2365
        return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . static::$LE . static::$LE . $this->MIMEBody;
2366
    }
2367
2368
    /**
2369
     * Create a unique ID to use for boundaries.
2370
     *
2371
     * @return string
2372
     */
2373
    protected function generateId()
2374
    {
2375
        $len = 32; //32 bytes = 256 bits
2376
        if (function_exists('random_bytes')) {
2377
            $bytes = random_bytes($len);
2378
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
2379
            $bytes = openssl_random_pseudo_bytes($len);
2380
        } else {
2381
            //Use a hash to force the length to the same as the other methods
2382
            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
2383
        }
2384
2385
        //We don't care about messing up base64 format here, just want a random string
2386
        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
2387
    }
2388
2389
    /**
2390
     * Assemble the message body.
2391
     * Returns an empty string on failure.
2392
     *
2393
     * @throws Exception
2394
     *
2395
     * @return string The assembled message body
2396
     */
2397
    public function createBody()
2398
    {
2399
        $body = '';
2400
        //Create unique IDs and preset boundaries
2401
        $this->uniqueid = $this->generateId();
2402
        $this->boundary[1] = 'b1_' . $this->uniqueid;
2403
        $this->boundary[2] = 'b2_' . $this->uniqueid;
2404
        $this->boundary[3] = 'b3_' . $this->uniqueid;
2405
2406
        if ($this->sign_key_file) {
2407
            $body .= $this->getMailMIME() . static::$LE;
2408
        }
2409
2410
        $this->setWordWrap();
2411
2412
        $bodyEncoding = $this->Encoding;
2413
        $bodyCharSet = $this->CharSet;
2414
        //Can we do a 7-bit downgrade?
2415
        if ('8bit' == $bodyEncoding and !$this->has8bitChars($this->Body)) {
2416
            $bodyEncoding = '7bit';
2417
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2418
            $bodyCharSet = 'us-ascii';
2419
        }
2420
        //If lines are too long, and we're not already using an encoding that will shorten them,
2421
        //change to quoted-printable transfer encoding for the body part only
2422
        if ('base64' != $this->Encoding and static::hasLineLongerThanMax($this->Body)) {
2423
            $bodyEncoding = 'quoted-printable';
2424
        }
2425
2426
        $altBodyEncoding = $this->Encoding;
2427
        $altBodyCharSet = $this->CharSet;
2428
        //Can we do a 7-bit downgrade?
2429
        if ('8bit' == $altBodyEncoding and !$this->has8bitChars($this->AltBody)) {
2430
            $altBodyEncoding = '7bit';
2431
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2432
            $altBodyCharSet = 'us-ascii';
2433
        }
2434
        //If lines are too long, and we're not already using an encoding that will shorten them,
2435
        //change to quoted-printable transfer encoding for the alt body part only
2436
        if ('base64' != $altBodyEncoding and static::hasLineLongerThanMax($this->AltBody)) {
2437
            $altBodyEncoding = 'quoted-printable';
2438
        }
2439
        //Use this as a preamble in all multipart message types
2440
        $mimepre = 'This is a multi-part message in MIME format.' . static::$LE;
2441
        switch ($this->message_type) {
2442
            case 'inline':
2443
                $body .= $mimepre;
2444
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2445
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2446
                $body .= static::$LE;
2447
                $body .= $this->attachAll('inline', $this->boundary[1]);
2448
                break;
2449
            case 'attach':
2450
                $body .= $mimepre;
2451
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2452
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2453
                $body .= static::$LE;
2454
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2455
                break;
2456
            case 'inline_attach':
2457
                $body .= $mimepre;
2458
                $body .= $this->textLine('--' . $this->boundary[1]);
2459
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2460
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2461
                $body .= static::$LE;
2462
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
2463
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2464
                $body .= static::$LE;
2465
                $body .= $this->attachAll('inline', $this->boundary[2]);
2466
                $body .= static::$LE;
2467
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2468
                break;
2469
            case 'alt':
2470
                $body .= $mimepre;
2471
                $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2472
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2473
                $body .= static::$LE;
2474
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, 'text/html', $bodyEncoding);
2475
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2476
                $body .= static::$LE;
2477
                if (!empty($this->Ical)) {
2478
                    $body .= $this->getBoundary($this->boundary[1], '', 'text/calendar; method=REQUEST', '');
2479
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2480
                    $body .= static::$LE;
2481
                }
2482
                $body .= $this->endBoundary($this->boundary[1]);
2483
                break;
2484
            case 'alt_inline':
2485
                $body .= $mimepre;
2486
                $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2487
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2488
                $body .= static::$LE;
2489
                $body .= $this->textLine('--' . $this->boundary[1]);
2490
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2491
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2492
                $body .= static::$LE;
2493
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding);
2494
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2495
                $body .= static::$LE;
2496
                $body .= $this->attachAll('inline', $this->boundary[2]);
2497
                $body .= static::$LE;
2498
                $body .= $this->endBoundary($this->boundary[1]);
2499
                break;
2500
            case 'alt_attach':
2501
                $body .= $mimepre;
2502
                $body .= $this->textLine('--' . $this->boundary[1]);
2503
                $body .= $this->headerLine('Content-Type', 'multipart/alternative;');
2504
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2505
                $body .= static::$LE;
2506
                $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2507
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2508
                $body .= static::$LE;
2509
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding);
2510
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2511
                $body .= static::$LE;
2512
                if (!empty($this->Ical)) {
2513
                    $body .= $this->getBoundary($this->boundary[2], '', 'text/calendar; method=REQUEST', '');
2514
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2515
                }
2516
                $body .= $this->endBoundary($this->boundary[2]);
2517
                $body .= static::$LE;
2518
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2519
                break;
2520
            case 'alt_inline_attach':
2521
                $body .= $mimepre;
2522
                $body .= $this->textLine('--' . $this->boundary[1]);
2523
                $body .= $this->headerLine('Content-Type', 'multipart/alternative;');
2524
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2525
                $body .= static::$LE;
2526
                $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2527
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2528
                $body .= static::$LE;
2529
                $body .= $this->textLine('--' . $this->boundary[2]);
2530
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2531
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"');
2532
                $body .= static::$LE;
2533
                $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, 'text/html', $bodyEncoding);
2534
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2535
                $body .= static::$LE;
2536
                $body .= $this->attachAll('inline', $this->boundary[3]);
2537
                $body .= static::$LE;
2538
                $body .= $this->endBoundary($this->boundary[2]);
2539
                $body .= static::$LE;
2540
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2541
                break;
2542
            default:
2543
                // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
2544
                //Reset the `Encoding` property in case we changed it for line length reasons
2545
                $this->Encoding = $bodyEncoding;
2546
                $body .= $this->encodeString($this->Body, $this->Encoding);
2547
                break;
2548
        }
2549
2550
        if ($this->isError()) {
2551
            $body = '';
2552
            if ($this->exceptions) {
2553
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
2554
            }
2555
        } elseif ($this->sign_key_file) {
2556
            try {
2557
                if (!defined('PKCS7_TEXT')) {
2558
                    throw new Exception($this->lang('extension_missing') . 'openssl');
2559
                }
2560
                // @TODO would be nice to use php://temp streams here
2561
                $file = tempnam(sys_get_temp_dir(), 'mail');
2562
                if (false === file_put_contents($file, $body)) {
2563
                    throw new Exception($this->lang('signing') . ' Could not write temp file');
2564
                }
2565
                $signed = tempnam(sys_get_temp_dir(), 'signed');
2566
                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
2567
                if (empty($this->sign_extracerts_file)) {
2568
                    $sign = @openssl_pkcs7_sign(
2569
                        $file,
2570
                        $signed,
2571
                        'file://' . realpath($this->sign_cert_file),
2572
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2573
                        []
2574
                    );
2575
                } else {
2576
                    $sign = @openssl_pkcs7_sign(
2577
                        $file,
2578
                        $signed,
2579
                        'file://' . realpath($this->sign_cert_file),
2580
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2581
                        [],
2582
                        PKCS7_DETACHED,
2583
                        $this->sign_extracerts_file
2584
                    );
2585
                }
2586
                @unlink($file);
2587
                if ($sign) {
2588
                    $body = file_get_contents($signed);
2589
                    @unlink($signed);
2590
                    //The message returned by openssl contains both headers and body, so need to split them up
2591
                    $parts = explode("\n\n", $body, 2);
2592
                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
2593
                    $body = $parts[1];
2594
                } else {
2595
                    @unlink($signed);
2596
                    throw new Exception($this->lang('signing') . openssl_error_string());
2597
                }
2598
            } catch (Exception $exc) {
2599
                $body = '';
2600
                if ($this->exceptions) {
2601
                    throw $exc;
2602
                }
2603
            }
2604
        }
2605
2606
        return $body;
2607
    }
2608
2609
    /**
2610
     * Return the start of a message boundary.
2611
     *
2612
     * @param string $boundary
2613
     * @param string $charSet
2614
     * @param string $contentType
2615
     * @param string $encoding
2616
     *
2617
     * @return string
2618
     */
2619
    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
2620
    {
2621
        $result = '';
2622
        if ('' == $charSet) {
2623
            $charSet = $this->CharSet;
2624
        }
2625
        if ('' == $contentType) {
2626
            $contentType = $this->ContentType;
2627
        }
2628
        if ('' == $encoding) {
2629
            $encoding = $this->Encoding;
2630
        }
2631
        $result .= $this->textLine('--' . $boundary);
2632
        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
2633
        $result .= static::$LE;
2634
        // RFC1341 part 5 says 7bit is assumed if not specified
2635
        if ('7bit' != $encoding) {
2636
            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
2637
        }
2638
        $result .= static::$LE;
2639
2640
        return $result;
2641
    }
2642
2643
    /**
2644
     * Return the end of a message boundary.
2645
     *
2646
     * @param string $boundary
2647
     *
2648
     * @return string
2649
     */
2650
    protected function endBoundary($boundary)
2651
    {
2652
        return static::$LE . '--' . $boundary . '--' . static::$LE;
2653
    }
2654
2655
    /**
2656
     * Set the message type.
2657
     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
2658
     */
2659
    protected function setMessageType()
2660
    {
2661
        $type = [];
2662
        if ($this->alternativeExists()) {
2663
            $type[] = 'alt';
2664
        }
2665
        if ($this->inlineImageExists()) {
2666
            $type[] = 'inline';
2667
        }
2668
        if ($this->attachmentExists()) {
2669
            $type[] = 'attach';
2670
        }
2671
        $this->message_type = implode('_', $type);
2672
        if ('' == $this->message_type) {
2673
            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
2674
            $this->message_type = 'plain';
2675
        }
2676
    }
2677
2678
    /**
2679
     * Format a header line.
2680
     *
2681
     * @param string     $name
2682
     * @param string|int $value
2683
     *
2684
     * @return string
2685
     */
2686
    public function headerLine($name, $value)
2687
    {
2688
        return $name . ': ' . $value . static::$LE;
2689
    }
2690
2691
    /**
2692
     * Return a formatted mail line.
2693
     *
2694
     * @param string $value
2695
     *
2696
     * @return string
2697
     */
2698
    public function textLine($value)
2699
    {
2700
        return $value . static::$LE;
2701
    }
2702
2703
    /**
2704
     * Add an attachment from a path on the filesystem.
2705
     * Never use a user-supplied path to a file!
2706
     * Returns false if the file could not be found or read.
2707
     *
2708
     * @param string $path        Path to the attachment
2709
     * @param string $name        Overrides the attachment name
2710
     * @param string $encoding    File encoding (see $Encoding)
2711
     * @param string $type        File extension (MIME) type
2712
     * @param string $disposition Disposition to use
2713
     *
2714
     * @throws Exception
2715
     *
2716
     * @return bool
2717
     */
2718
    public function addAttachment($path, $name = '', $encoding = 'base64', $type = '', $disposition = 'attachment')
2719
    {
2720
        try {
2721
            if (!@is_file($path)) {
2722
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
2723
            }
2724
2725
            // If a MIME type is not specified, try to work it out from the file name
2726
            if ('' == $type) {
2727
                $type = static::filenameToType($path);
2728
            }
2729
2730
            $filename = basename($path);
2731
            if ('' == $name) {
2732
                $name = $filename;
2733
            }
2734
2735
            $this->attachment[] = [
2736
                0 => $path,
2737
                1 => $filename,
2738
                2 => $name,
2739
                3 => $encoding,
2740
                4 => $type,
2741
                5 => false, // isStringAttachment
2742
                6 => $disposition,
2743
                7 => $name,
2744
            ];
2745
        } catch (Exception $exc) {
2746
            $this->setError($exc->getMessage());
2747
            $this->edebug($exc->getMessage());
2748
            if ($this->exceptions) {
2749
                throw $exc;
2750
            }
2751
2752
            return false;
2753
        }
2754
2755
        return true;
2756
    }
2757
2758
    /**
2759
     * Return the array of attachments.
2760
     *
2761
     * @return array
2762
     */
2763
    public function getAttachments()
2764
    {
2765
        return $this->attachment;
2766
    }
2767
2768
    /**
2769
     * Attach all file, string, and binary attachments to the message.
2770
     * Returns an empty string on failure.
2771
     *
2772
     * @param string $disposition_type
2773
     * @param string $boundary
2774
     *
2775
     * @return string
2776
     */
2777
    protected function attachAll($disposition_type, $boundary)
0 ignored issues
show
Complexity introduced by
This operation has 770 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
2778
    {
2779
        // Return text of body
2780
        $mime = [];
2781
        $cidUniq = [];
2782
        $incl = [];
2783
2784
        // Add all attachments
2785
        foreach ($this->attachment as $attachment) {
2786
            // Check if it is a valid disposition_filter
2787
            if ($attachment[6] == $disposition_type) {
2788
                // Check for string attachment
2789
                $string = '';
2790
                $path = '';
2791
                $bString = $attachment[5];
2792
                if ($bString) {
2793
                    $string = $attachment[0];
2794
                } else {
2795
                    $path = $attachment[0];
2796
                }
2797
2798
                $inclhash = hash('sha256', serialize($attachment));
2799
                if (in_array($inclhash, $incl)) {
2800
                    continue;
2801
                }
2802
                $incl[] = $inclhash;
2803
                $name = $attachment[2];
2804
                $encoding = $attachment[3];
2805
                $type = $attachment[4];
2806
                $disposition = $attachment[6];
2807
                $cid = $attachment[7];
2808
                if ('inline' == $disposition and array_key_exists($cid, $cidUniq)) {
2809
                    continue;
2810
                }
2811
                $cidUniq[$cid] = true;
2812
2813
                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
2814
                //Only include a filename property if we have one
2815
                if (!empty($name)) {
2816
                    $mime[] = sprintf(
2817
                        'Content-Type: %s; name="%s"%s',
2818
                        $type,
2819
                        $this->encodeHeader($this->secureHeader($name)),
2820
                        static::$LE
2821
                    );
2822
                } else {
2823
                    $mime[] = sprintf(
2824
                        'Content-Type: %s%s',
2825
                        $type,
2826
                        static::$LE
2827
                    );
2828
                }
2829
                // RFC1341 part 5 says 7bit is assumed if not specified
2830
                if ('7bit' != $encoding) {
2831
                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
2832
                }
2833
2834
                $mime[] = sprintf('Content-ID: <%s>%s', $cid, static::$LE);
2835
2836
                // If a filename contains any of these chars, it should be quoted,
2837
                // but not otherwise: RFC2183 & RFC2045 5.1
2838
                // Fixes a warning in IETF's msglint MIME checker
2839
                // Allow for bypassing the Content-Disposition header totally
2840
                if (!(empty($disposition))) {
2841
                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
2842
                    if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) {
2843
                        $mime[] = sprintf(
2844
                            'Content-Disposition: %s; filename="%s"%s',
2845
                            $disposition,
2846
                            $encoded_name,
2847
                            static::$LE . static::$LE
2848
                        );
2849
                    } else {
2850
                        if (!empty($encoded_name)) {
2851
                            $mime[] = sprintf(
2852
                                'Content-Disposition: %s; filename=%s%s',
2853
                                $disposition,
2854
                                $encoded_name,
2855
                                static::$LE . static::$LE
2856
                            );
2857
                        } else {
2858
                            $mime[] = sprintf(
2859
                                'Content-Disposition: %s%s',
2860
                                $disposition,
2861
                                static::$LE . static::$LE
2862
                            );
2863
                        }
2864
                    }
2865
                } else {
2866
                    $mime[] = static::$LE;
2867
                }
2868
2869
                // Encode as string attachment
2870
                if ($bString) {
2871
                    $mime[] = $this->encodeString($string, $encoding);
2872
                } else {
2873
                    $mime[] = $this->encodeFile($path, $encoding);
2874
                }
2875
                if ($this->isError()) {
2876
                    return '';
2877
                }
2878
                $mime[] = static::$LE;
2879
            }
2880
        }
2881
2882
        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
2883
2884
        return implode('', $mime);
2885
    }
2886
2887
    /**
2888
     * Encode a file attachment in requested format.
2889
     * Returns an empty string on failure.
2890
     *
2891
     * @param string $path     The full path to the file
2892
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
2893
     *
2894
     * @throws Exception
2895
     *
2896
     * @return string
2897
     */
2898
    protected function encodeFile($path, $encoding = 'base64')
2899
    {
2900
        try {
2901
            if (!file_exists($path)) {
2902
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
2903
            }
2904
            $file_buffer = file_get_contents($path);
2905
            if (false === $file_buffer) {
2906
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
2907
            }
2908
            $file_buffer = $this->encodeString($file_buffer, $encoding);
2909
2910
            return $file_buffer;
2911
        } catch (Exception $exc) {
2912
            $this->setError($exc->getMessage());
2913
2914
            return '';
2915
        }
2916
    }
2917
2918
    /**
2919
     * Encode a string in requested format.
2920
     * Returns an empty string on failure.
2921
     *
2922
     * @param string $str      The text to encode
2923
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable
2924
     *
2925
     * @return string
2926
     */
2927
    public function encodeString($str, $encoding = 'base64')
2928
    {
2929
        $encoded = '';
2930
        switch (strtolower($encoding)) {
2931
            case 'base64':
2932
                $encoded = chunk_split(
2933
                    base64_encode($str),
2934
                    static::STD_LINE_LENGTH - strlen(static::$LE),
2935
                    static::$LE
2936
                );
2937
                break;
2938
            case '7bit':
2939
            case '8bit':
2940
                $encoded = static::normalizeBreaks($str);
2941
                // Make sure it ends with a line break
2942
                if (substr($encoded, -(strlen(static::$LE))) != static::$LE) {
2943
                    $encoded .= static::$LE;
2944
                }
2945
                break;
2946
            case 'binary':
2947
                $encoded = $str;
2948
                break;
2949
            case 'quoted-printable':
2950
                $encoded = $this->encodeQP($str);
2951
                break;
2952
            default:
2953
                $this->setError($this->lang('encoding') . $encoding);
2954
                break;
2955
        }
2956
2957
        return $encoded;
2958
    }
2959
2960
    /**
2961
     * Encode a header value (not including its label) optimally.
2962
     * Picks shortest of Q, B, or none. Result includes folding if needed.
2963
     * See RFC822 definitions for phrase, comment and text positions.
2964
     *
2965
     * @param string $str      The header value to encode
2966
     * @param string $position What context the string will be used in
2967
     *
2968
     * @return string
2969
     */
2970
    public function encodeHeader($str, $position = 'text')
0 ignored issues
show
Complexity introduced by
This operation has 210 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
2971
    {
2972
        $matchcount = 0;
2973
        switch (strtolower($position)) {
2974
            case 'phrase':
2975
                if (!preg_match('/[\200-\377]/', $str)) {
2976
                    // Can't use addslashes as we don't know the value of magic_quotes_sybase
2977
                    $encoded = addcslashes($str, "\0..\37\177\\\"");
2978
                    if (($str == $encoded) and !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
2979
                        return $encoded;
2980
                    }
2981
2982
                    return "\"$encoded\"";
2983
                }
2984
                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
2985
                break;
2986
            /* @noinspection PhpMissingBreakStatementInspection */
2987
            case 'comment':
2988
                $matchcount = preg_match_all('/[()"]/', $str, $matches);
2989
            //fallthrough
2990
            case 'text':
2991
            default:
2992
                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
2993
                break;
2994
        }
2995
2996
        //RFCs specify a maximum line length of 78 chars, however mail() will sometimes
2997
        //corrupt messages with headers longer than 65 chars. See #818
2998
        $lengthsub = 'mail' == $this->Mailer ? 13 : 0;
2999
        $maxlen = static::STD_LINE_LENGTH - $lengthsub;
3000
        // Try to select the encoding which should produce the shortest output
3001
        if ($matchcount > strlen($str) / 3) {
3002
            // More than a third of the content will need encoding, so B encoding will be most efficient
3003
            $encoding = 'B';
3004
            //This calculation is:
3005
            // max line length
3006
            // - shorten to avoid mail() corruption
3007
            // - Q/B encoding char overhead ("` =?<charset>?[QB]?<content>?=`")
3008
            // - charset name length
3009
            $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet);
3010
            if ($this->hasMultiBytes($str)) {
3011
                // Use a custom function which correctly encodes and wraps long
3012
                // multibyte strings without breaking lines within a character
3013
                $encoded = $this->base64EncodeWrapMB($str, "\n");
3014
            } else {
3015
                $encoded = base64_encode($str);
3016
                $maxlen -= $maxlen % 4;
3017
                $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
3018
            }
3019
            $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
3020
        } elseif ($matchcount > 0) {
3021
            //1 or more chars need encoding, use Q-encode
3022
            $encoding = 'Q';
3023
            //Recalc max line length for Q encoding - see comments on B encode
3024
            $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet);
3025
            $encoded = $this->encodeQ($str, $position);
3026
            $encoded = $this->wrapText($encoded, $maxlen, true);
3027
            $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
3028
            $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
3029
        } elseif (strlen($str) > $maxlen) {
3030
            //No chars need encoding, but line is too long, so fold it
3031
            $encoded = trim($this->wrapText($str, $maxlen, false));
3032
            if ($str == $encoded) {
3033
                //Wrapping nicely didn't work, wrap hard instead
3034
                $encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE));
3035
            }
3036
            $encoded = str_replace(static::$LE, "\n", trim($encoded));
3037
            $encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded);
3038
        } else {
3039
            //No reformatting needed
3040
            return $str;
3041
        }
3042
3043
        return trim(static::normalizeBreaks($encoded));
3044
    }
3045
3046
    /**
3047
     * Check if a string contains multi-byte characters.
3048
     *
3049
     * @param string $str multi-byte text to wrap encode
3050
     *
3051
     * @return bool
3052
     */
3053
    public function hasMultiBytes($str)
3054
    {
3055
        if (function_exists('mb_strlen')) {
3056
            return strlen($str) > mb_strlen($str, $this->CharSet);
3057
        }
3058
3059
        // Assume no multibytes (we can't handle without mbstring functions anyway)
3060
        return false;
3061
    }
3062
3063
    /**
3064
     * Does a string contain any 8-bit chars (in any charset)?
3065
     *
3066
     * @param string $text
3067
     *
3068
     * @return bool
3069
     */
3070
    public function has8bitChars($text)
3071
    {
3072
        return (bool) preg_match('/[\x80-\xFF]/', $text);
3073
    }
3074
3075
    /**
3076
     * Encode and wrap long multibyte strings for mail headers
3077
     * without breaking lines within a character.
3078
     * Adapted from a function by paravoid.
3079
     *
3080
     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
3081
     *
3082
     * @param string $str       multi-byte text to wrap encode
3083
     * @param string $linebreak string to use as linefeed/end-of-line
3084
     *
3085
     * @return string
3086
     */
3087
    public function base64EncodeWrapMB($str, $linebreak = null)
3088
    {
3089
        $start = '=?' . $this->CharSet . '?B?';
3090
        $end = '?=';
3091
        $encoded = '';
3092
        if (null === $linebreak) {
3093
            $linebreak = static::$LE;
3094
        }
3095
3096
        $mb_length = mb_strlen($str, $this->CharSet);
3097
        // Each line must have length <= 75, including $start and $end
3098
        $length = 75 - strlen($start) - strlen($end);
3099
        // Average multi-byte ratio
3100
        $ratio = $mb_length / strlen($str);
3101
        // Base64 has a 4:3 ratio
3102
        $avgLength = floor($length * $ratio * .75);
3103
3104
        for ($i = 0; $i < $mb_length; $i += $offset) {
3105
            $lookBack = 0;
3106
            do {
3107
                $offset = $avgLength - $lookBack;
3108
                $chunk = mb_substr($str, $i, $offset, $this->CharSet);
3109
                $chunk = base64_encode($chunk);
3110
                ++$lookBack;
3111
            } while (strlen($chunk) > $length);
3112
            $encoded .= $chunk . $linebreak;
3113
        }
3114
3115
        // Chomp the last linefeed
3116
        return substr($encoded, 0, -strlen($linebreak));
3117
    }
3118
3119
    /**
3120
     * Encode a string in quoted-printable format.
3121
     * According to RFC2045 section 6.7.
3122
     *
3123
     * @param string $string The text to encode
3124
     *
3125
     * @return string
3126
     */
3127
    public function encodeQP($string)
3128
    {
3129
        return static::normalizeBreaks(quoted_printable_encode($string));
3130
    }
3131
3132
    /**
3133
     * Encode a string using Q encoding.
3134
     *
3135
     * @see http://tools.ietf.org/html/rfc2047#section-4.2
3136
     *
3137
     * @param string $str      the text to encode
3138
     * @param string $position Where the text is going to be used, see the RFC for what that means
3139
     *
3140
     * @return string
3141
     */
3142
    public function encodeQ($str, $position = 'text')
3143
    {
3144
        // There should not be any EOL in the string
3145
        $pattern = '';
3146
        $encoded = str_replace(["\r", "\n"], '', $str);
3147
        switch (strtolower($position)) {
3148
            case 'phrase':
3149
                // RFC 2047 section 5.3
3150
                $pattern = '^A-Za-z0-9!*+\/ -';
3151
                break;
3152
            /*
3153
             * RFC 2047 section 5.2.
3154
             * Build $pattern without including delimiters and []
3155
             */
3156
            /* @noinspection PhpMissingBreakStatementInspection */
3157
            case 'comment':
3158
                $pattern = '\(\)"';
3159
            /* Intentional fall through */
3160
            case 'text':
3161
            default:
3162
                // RFC 2047 section 5.1
3163
                // Replace every high ascii, control, =, ? and _ characters
3164
                $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
3165
                break;
3166
        }
3167
        $matches = [];
3168
        if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
3169
            // If the string contains an '=', make sure it's the first thing we replace
3170
            // so as to avoid double-encoding
3171
            $eqkey = array_search('=', $matches[0]);
3172
            if (false !== $eqkey) {
3173
                unset($matches[0][$eqkey]);
3174
                array_unshift($matches[0], '=');
3175
            }
3176
            foreach (array_unique($matches[0]) as $char) {
3177
                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
3178
            }
3179
        }
3180
        // Replace spaces with _ (more readable than =20)
3181
        // RFC 2047 section 4.2(2)
3182
        return str_replace(' ', '_', $encoded);
3183
    }
3184
3185
    /**
3186
     * Add a string or binary attachment (non-filesystem).
3187
     * This method can be used to attach ascii or binary data,
3188
     * such as a BLOB record from a database.
3189
     *
3190
     * @param string $string      String attachment data
3191
     * @param string $filename    Name of the attachment
3192
     * @param string $encoding    File encoding (see $Encoding)
3193
     * @param string $type        File extension (MIME) type
3194
     * @param string $disposition Disposition to use
3195
     */
3196
    public function addStringAttachment(
3197
        $string,
3198
        $filename,
3199
        $encoding = 'base64',
3200
        $type = '',
3201
        $disposition = 'attachment'
3202
    ) {
3203
        // If a MIME type is not specified, try to work it out from the file name
3204
        if ('' == $type) {
3205
            $type = static::filenameToType($filename);
3206
        }
3207
        // Append to $attachment array
3208
        $this->attachment[] = [
3209
            0 => $string,
3210
            1 => $filename,
3211
            2 => basename($filename),
3212
            3 => $encoding,
3213
            4 => $type,
3214
            5 => true, // isStringAttachment
3215
            6 => $disposition,
3216
            7 => 0,
3217
        ];
3218
    }
3219
3220
    /**
3221
     * Add an embedded (inline) attachment from a file.
3222
     * This can include images, sounds, and just about any other document type.
3223
     * These differ from 'regular' attachments in that they are intended to be
3224
     * displayed inline with the message, not just attached for download.
3225
     * This is used in HTML messages that embed the images
3226
     * the HTML refers to using the $cid value.
3227
     * Never use a user-supplied path to a file!
3228
     *
3229
     * @param string $path        Path to the attachment
3230
     * @param string $cid         Content ID of the attachment; Use this to reference
3231
     *                            the content when using an embedded image in HTML
3232
     * @param string $name        Overrides the attachment name
3233
     * @param string $encoding    File encoding (see $Encoding)
3234
     * @param string $type        File MIME type
3235
     * @param string $disposition Disposition to use
3236
     *
3237
     * @return bool True on successfully adding an attachment
3238
     */
3239
    public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = '', $disposition = 'inline')
3240
    {
3241
        if (!@is_file($path)) {
3242
            $this->setError($this->lang('file_access') . $path);
3243
3244
            return false;
3245
        }
3246
3247
        // If a MIME type is not specified, try to work it out from the file name
3248
        if ('' == $type) {
3249
            $type = static::filenameToType($path);
3250
        }
3251
3252
        $filename = basename($path);
3253
        if ('' == $name) {
3254
            $name = $filename;
3255
        }
3256
3257
        // Append to $attachment array
3258
        $this->attachment[] = [
3259
            0 => $path,
3260
            1 => $filename,
3261
            2 => $name,
3262
            3 => $encoding,
3263
            4 => $type,
3264
            5 => false, // isStringAttachment
3265
            6 => $disposition,
3266
            7 => $cid,
3267
        ];
3268
3269
        return true;
3270
    }
3271
3272
    /**
3273
     * Add an embedded stringified attachment.
3274
     * This can include images, sounds, and just about any other document type.
3275
     * Be sure to set the $type to an image type for images:
3276
     * JPEG images use 'image/jpeg', GIF uses 'image/gif', PNG uses 'image/png'.
3277
     *
3278
     * @param string $string      The attachment binary data
3279
     * @param string $cid         Content ID of the attachment; Use this to reference
3280
     *                            the content when using an embedded image in HTML
3281
     * @param string $name
3282
     * @param string $encoding    File encoding (see $Encoding)
3283
     * @param string $type        MIME type
3284
     * @param string $disposition Disposition to use
3285
     *
3286
     * @return bool True on successfully adding an attachment
3287
     */
3288
    public function addStringEmbeddedImage(
3289
        $string,
3290
        $cid,
3291
        $name = '',
3292
        $encoding = 'base64',
3293
        $type = '',
3294
        $disposition = 'inline'
3295
    ) {
3296
        // If a MIME type is not specified, try to work it out from the name
3297
        if ('' == $type and !empty($name)) {
3298
            $type = static::filenameToType($name);
3299
        }
3300
3301
        // Append to $attachment array
3302
        $this->attachment[] = [
3303
            0 => $string,
3304
            1 => $name,
3305
            2 => $name,
3306
            3 => $encoding,
3307
            4 => $type,
3308
            5 => true, // isStringAttachment
3309
            6 => $disposition,
3310
            7 => $cid,
3311
        ];
3312
3313
        return true;
3314
    }
3315
3316
    /**
3317
     * Check if an embedded attachment is present with this cid.
3318
     *
3319
     * @param string $cid
3320
     *
3321
     * @return bool
3322
     */
3323
    protected function cidExists($cid)
3324
    {
3325
        foreach ($this->attachment as $attachment) {
3326
            if ('inline' == $attachment[6] and $cid == $attachment[7]) {
3327
                return true;
3328
            }
3329
        }
3330
3331
        return false;
3332
    }
3333
3334
    /**
3335
     * Check if an inline attachment is present.
3336
     *
3337
     * @return bool
3338
     */
3339
    public function inlineImageExists()
3340
    {
3341
        foreach ($this->attachment as $attachment) {
3342
            if ($attachment[6] == 'inline') {
3343
                return true;
3344
            }
3345
        }
3346
3347
        return false;
3348
    }
3349
3350
    /**
3351
     * Check if an attachment (non-inline) is present.
3352
     *
3353
     * @return bool
3354
     */
3355
    public function attachmentExists()
3356
    {
3357
        foreach ($this->attachment as $attachment) {
3358
            if ($attachment[6] == 'attachment') {
3359
                return true;
3360
            }
3361
        }
3362
3363
        return false;
3364
    }
3365
3366
    /**
3367
     * Check if this message has an alternative body set.
3368
     *
3369
     * @return bool
3370
     */
3371
    public function alternativeExists()
3372
    {
3373
        return !empty($this->AltBody);
3374
    }
3375
3376
    /**
3377
     * Clear queued addresses of given kind.
3378
     *
3379
     * @param string $kind 'to', 'cc', or 'bcc'
3380
     */
3381
    public function clearQueuedAddresses($kind)
3382
    {
3383
        $this->RecipientsQueue = array_filter(
3384
            $this->RecipientsQueue,
3385
            function ($params) use ($kind) {
3386
                return $params[0] != $kind;
3387
            }
3388
        );
3389
    }
3390
3391
    /**
3392
     * Clear all To recipients.
3393
     */
3394
    public function clearAddresses()
3395
    {
3396
        foreach ($this->to as $to) {
3397
            unset($this->all_recipients[strtolower($to[0])]);
3398
        }
3399
        $this->to = [];
3400
        $this->clearQueuedAddresses('to');
3401
    }
3402
3403
    /**
3404
     * Clear all CC recipients.
3405
     */
3406
    public function clearCCs()
3407
    {
3408
        foreach ($this->cc as $cc) {
3409
            unset($this->all_recipients[strtolower($cc[0])]);
3410
        }
3411
        $this->cc = [];
3412
        $this->clearQueuedAddresses('cc');
3413
    }
3414
3415
    /**
3416
     * Clear all BCC recipients.
3417
     */
3418
    public function clearBCCs()
3419
    {
3420
        foreach ($this->bcc as $bcc) {
3421
            unset($this->all_recipients[strtolower($bcc[0])]);
3422
        }
3423
        $this->bcc = [];
3424
        $this->clearQueuedAddresses('bcc');
3425
    }
3426
3427
    /**
3428
     * Clear all ReplyTo recipients.
3429
     */
3430
    public function clearReplyTos()
3431
    {
3432
        $this->ReplyTo = [];
3433
        $this->ReplyToQueue = [];
3434
    }
3435
3436
    /**
3437
     * Clear all recipient types.
3438
     */
3439
    public function clearAllRecipients()
3440
    {
3441
        $this->to = [];
3442
        $this->cc = [];
3443
        $this->bcc = [];
3444
        $this->all_recipients = [];
3445
        $this->RecipientsQueue = [];
3446
    }
3447
3448
    /**
3449
     * Clear all filesystem, string, and binary attachments.
3450
     */
3451
    public function clearAttachments()
3452
    {
3453
        $this->attachment = [];
3454
    }
3455
3456
    /**
3457
     * Clear all custom headers.
3458
     */
3459
    public function clearCustomHeaders()
3460
    {
3461
        $this->CustomHeader = [];
3462
    }
3463
3464
    /**
3465
     * Add an error message to the error container.
3466
     *
3467
     * @param string $msg
3468
     */
3469
    protected function setError($msg)
3470
    {
3471
        ++$this->error_count;
3472
        if ('smtp' == $this->Mailer and null !== $this->smtp) {
3473
            $lasterror = $this->smtp->getError();
3474
            if (!empty($lasterror['error'])) {
3475
                $msg .= $this->lang('smtp_error') . $lasterror['error'];
3476
                if (!empty($lasterror['detail'])) {
3477
                    $msg .= ' Detail: ' . $lasterror['detail'];
3478
                }
3479
                if (!empty($lasterror['smtp_code'])) {
3480
                    $msg .= ' SMTP code: ' . $lasterror['smtp_code'];
3481
                }
3482
                if (!empty($lasterror['smtp_code_ex'])) {
3483
                    $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex'];
3484
                }
3485
            }
3486
        }
3487
        $this->ErrorInfo = $msg;
3488
    }
3489
3490
    /**
3491
     * Return an RFC 822 formatted date.
3492
     *
3493
     * @return string
3494
     */
3495
    public static function rfcDate()
3496
    {
3497
        // Set the time zone to whatever the default is to avoid 500 errors
3498
        // Will default to UTC if it's not set properly in php.ini
3499
        date_default_timezone_set(@date_default_timezone_get());
3500
3501
        return date('D, j M Y H:i:s O');
3502
    }
3503
3504
    /**
3505
     * Get the server hostname.
3506
     * Returns 'localhost.localdomain' if unknown.
3507
     *
3508
     * @return string
3509
     */
3510
    protected function serverHostname()
0 ignored issues
show
Coding Style introduced by
serverHostname uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
3511
    {
3512
        $result = '';
3513
        if (!empty($this->Hostname)) {
3514
            $result = $this->Hostname;
3515
        } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER)) {
3516
            $result = $_SERVER['SERVER_NAME'];
3517
        } elseif (function_exists('gethostname') and gethostname() !== false) {
3518
            $result = gethostname();
3519
        } elseif (php_uname('n') !== false) {
3520
            $result = php_uname('n');
3521
        }
3522
        if (!static::isValidHost($result)) {
3523
            return 'localhost.localdomain';
3524
        }
3525
3526
        return $result;
3527
    }
3528
3529
    /**
3530
     * Validate whether a string contains a valid value to use as a hostname or IP address.
3531
     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
3532
     *
3533
     * @param string $host The host name or IP address to check
3534
     *
3535
     * @return bool
3536
     */
3537
    public static function isValidHost($host)
3538
    {
3539
        //Simple syntax limits
3540
        if (empty($host)
3541
            or !is_string($host)
3542
            or strlen($host) > 256
3543
        ) {
3544
            return false;
3545
        }
3546
        //Looks like a bracketed IPv6 address
3547
        if (trim($host, '[]') != $host) {
3548
            return (bool) filter_var(trim($host, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
3549
        }
3550
        //If removing all the dots results in a numeric string, it must be an IPv4 address.
3551
        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
3552
        if (is_numeric(str_replace('.', '', $host))) {
3553
            //Is it a valid IPv4 address?
3554
            return (bool) filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
3555
        }
3556
        if (filter_var('http://' . $host, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)) {
3557
            //Is it a syntactically valid hostname?
3558
            return true;
3559
        }
3560
3561
        return false;
3562
    }
3563
3564
    /**
3565
     * Get an error message in the current language.
3566
     *
3567
     * @param string $key
3568
     *
3569
     * @return string
3570
     */
3571
    protected function lang($key)
3572
    {
3573
        if (count($this->language) < 1) {
3574
            $this->setLanguage('en'); // set the default language
3575
        }
3576
3577
        if (array_key_exists($key, $this->language)) {
3578
            if ('smtp_connect_failed' == $key) {
3579
                //Include a link to troubleshooting docs on SMTP connection failure
3580
                //this is by far the biggest cause of support questions
3581
                //but it's usually not PHPMailer's fault.
3582
                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
3583
            }
3584
3585
            return $this->language[$key];
3586
        }
3587
3588
        //Return the key as a fallback
3589
        return $key;
3590
    }
3591
3592
    /**
3593
     * Check if an error occurred.
3594
     *
3595
     * @return bool True if an error did occur
3596
     */
3597
    public function isError()
3598
    {
3599
        return $this->error_count > 0;
3600
    }
3601
3602
    /**
3603
     * Add a custom header.
3604
     * $name value can be overloaded to contain
3605
     * both header name and value (name:value).
3606
     *
3607
     * @param string      $name  Custom header name
3608
     * @param string|null $value Header value
3609
     */
3610
    public function addCustomHeader($name, $value = null)
3611
    {
3612
        if (null === $value) {
3613
            // Value passed in as name:value
3614
            $this->CustomHeader[] = explode(':', $name, 2);
3615
        } else {
3616
            $this->CustomHeader[] = [$name, $value];
3617
        }
3618
    }
3619
3620
    /**
3621
     * Returns all custom headers.
3622
     *
3623
     * @return array
3624
     */
3625
    public function getCustomHeaders()
3626
    {
3627
        return $this->CustomHeader;
3628
    }
3629
3630
    /**
3631
     * Create a message body from an HTML string.
3632
     * Automatically inlines images and creates a plain-text version by converting the HTML,
3633
     * overwriting any existing values in Body and AltBody.
3634
     * Do not source $message content from user input!
3635
     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
3636
     * will look for an image file in $basedir/images/a.png and convert it to inline.
3637
     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
3638
     * Converts data-uri images into embedded attachments.
3639
     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
3640
     *
3641
     * @param string        $message  HTML message string
3642
     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
3643
     * @param bool|callable $advanced Whether to use the internal HTML to text converter
3644
     *                                or your own custom converter @see PHPMailer::html2text()
3645
     *
3646
     * @return string $message The transformed message Body
3647
     */
3648
    public function msgHTML($message, $basedir = '', $advanced = false)
0 ignored issues
show
Complexity introduced by
This operation has 2168 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
3649
    {
3650
        preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images);
3651
        if (array_key_exists(2, $images)) {
3652
            if (strlen($basedir) > 1 && substr($basedir, -1) != '/') {
3653
                // Ensure $basedir has a trailing /
3654
                $basedir .= '/';
3655
            }
3656
            foreach ($images[2] as $imgindex => $url) {
3657
                // Convert data URIs into embedded images
3658
                //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
3659
                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
3660
                    if (count($match) == 4 and 'base64' == $match[2]) {
3661
                        $data = base64_decode($match[3]);
3662
                    } elseif ('' == $match[2]) {
3663
                        $data = rawurldecode($match[3]);
3664
                    } else {
3665
                        //Not recognised so leave it alone
3666
                        continue;
3667
                    }
3668
                    //Hash the decoded data, not the URL so that the same data-URI image used in multiple places
3669
                    //will only be embedded once, even if it used a different encoding
3670
                    $cid = hash('sha256', $data) . '@phpmailer.0'; // RFC2392 S 2
3671
3672
                    if (!$this->cidExists($cid)) {
3673
                        $this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, 'base64', $match[1]);
3674
                    }
3675
                    $message = str_replace(
3676
                        $images[0][$imgindex],
3677
                        $images[1][$imgindex] . '="cid:' . $cid . '"',
3678
                        $message
3679
                    );
3680
                    continue;
3681
                }
3682
                if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
3683
                    !empty($basedir)
3684
                    // Ignore URLs containing parent dir traversal (..)
3685
                    and (strpos($url, '..') === false)
3686
                    // Do not change urls that are already inline images
3687
                    and substr($url, 0, 4) !== 'cid:'
3688
                    // Do not change absolute URLs, including anonymous protocol
3689
                    and !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
3690
                ) {
3691
                    $filename = basename($url);
3692
                    $directory = dirname($url);
3693
                    if ('.' == $directory) {
3694
                        $directory = '';
3695
                    }
3696
                    $cid = hash('sha256', $url) . '@phpmailer.0'; // RFC2392 S 2
3697
                    if (strlen($basedir) > 1 and substr($basedir, -1) != '/') {
3698
                        $basedir .= '/';
3699
                    }
3700
                    if (strlen($directory) > 1 and substr($directory, -1) != '/') {
3701
                        $directory .= '/';
3702
                    }
3703
                    if ($this->addEmbeddedImage(
3704
                        $basedir . $directory . $filename,
3705
                        $cid,
3706
                        $filename,
3707
                        'base64',
3708
                        static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
3709
                    )
3710
                    ) {
3711
                        $message = preg_replace(
3712
                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
3713
                            $images[1][$imgindex] . '="cid:' . $cid . '"',
3714
                            $message
3715
                        );
3716
                    }
3717
                }
3718
            }
3719
        }
3720
        $this->isHTML(true);
3721
        // Convert all message body line breaks to LE, makes quoted-printable encoding work much better
3722
        $this->Body = static::normalizeBreaks($message);
3723
        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
3724
        if (!$this->alternativeExists()) {
3725
            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
3726
                . static::$LE;
3727
        }
3728
3729
        return $this->Body;
3730
    }
3731
3732
    /**
3733
     * Convert an HTML string into plain text.
3734
     * This is used by msgHTML().
3735
     * Note - older versions of this function used a bundled advanced converter
3736
     * which was removed for license reasons in #232.
3737
     * Example usage:
3738
     *
3739
     * ```php
3740
     * // Use default conversion
3741
     * $plain = $mail->html2text($html);
3742
     * // Use your own custom converter
3743
     * $plain = $mail->html2text($html, function($html) {
3744
     *     $converter = new MyHtml2text($html);
3745
     *     return $converter->get_text();
3746
     * });
3747
     * ```
3748
     *
3749
     * @param string        $html     The HTML text to convert
3750
     * @param bool|callable $advanced Any boolean value to use the internal converter,
3751
     *                                or provide your own callable for custom conversion
3752
     *
3753
     * @return string
3754
     */
3755
    public function html2text($html, $advanced = false)
3756
    {
3757
        if (is_callable($advanced)) {
3758
            return call_user_func($advanced, $html);
3759
        }
3760
3761
        return html_entity_decode(
3762
            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
3763
            ENT_QUOTES,
3764
            $this->CharSet
3765
        );
3766
    }
3767
3768
    /**
3769
     * Get the MIME type for a file extension.
3770
     *
3771
     * @param string $ext File extension
3772
     *
3773
     * @return string MIME type of file
3774
     */
3775
    public static function _mime_types($ext = '')
3776
    {
3777
        $mimes = [
3778
            'xl' => 'application/excel',
3779
            'js' => 'application/javascript',
3780
            'hqx' => 'application/mac-binhex40',
3781
            'cpt' => 'application/mac-compactpro',
3782
            'bin' => 'application/macbinary',
3783
            'doc' => 'application/msword',
3784
            'word' => 'application/msword',
3785
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
3786
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
3787
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
3788
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
3789
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
3790
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
3791
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
3792
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
3793
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
3794
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
3795
            'class' => 'application/octet-stream',
3796
            'dll' => 'application/octet-stream',
3797
            'dms' => 'application/octet-stream',
3798
            'exe' => 'application/octet-stream',
3799
            'lha' => 'application/octet-stream',
3800
            'lzh' => 'application/octet-stream',
3801
            'psd' => 'application/octet-stream',
3802
            'sea' => 'application/octet-stream',
3803
            'so' => 'application/octet-stream',
3804
            'oda' => 'application/oda',
3805
            'pdf' => 'application/pdf',
3806
            'ai' => 'application/postscript',
3807
            'eps' => 'application/postscript',
3808
            'ps' => 'application/postscript',
3809
            'smi' => 'application/smil',
3810
            'smil' => 'application/smil',
3811
            'mif' => 'application/vnd.mif',
3812
            'xls' => 'application/vnd.ms-excel',
3813
            'ppt' => 'application/vnd.ms-powerpoint',
3814
            'wbxml' => 'application/vnd.wap.wbxml',
3815
            'wmlc' => 'application/vnd.wap.wmlc',
3816
            'dcr' => 'application/x-director',
3817
            'dir' => 'application/x-director',
3818
            'dxr' => 'application/x-director',
3819
            'dvi' => 'application/x-dvi',
3820
            'gtar' => 'application/x-gtar',
3821
            'php3' => 'application/x-httpd-php',
3822
            'php4' => 'application/x-httpd-php',
3823
            'php' => 'application/x-httpd-php',
3824
            'phtml' => 'application/x-httpd-php',
3825
            'phps' => 'application/x-httpd-php-source',
3826
            'swf' => 'application/x-shockwave-flash',
3827
            'sit' => 'application/x-stuffit',
3828
            'tar' => 'application/x-tar',
3829
            'tgz' => 'application/x-tar',
3830
            'xht' => 'application/xhtml+xml',
3831
            'xhtml' => 'application/xhtml+xml',
3832
            'zip' => 'application/zip',
3833
            'mid' => 'audio/midi',
3834
            'midi' => 'audio/midi',
3835
            'mp2' => 'audio/mpeg',
3836
            'mp3' => 'audio/mpeg',
3837
            'mpga' => 'audio/mpeg',
3838
            'aif' => 'audio/x-aiff',
3839
            'aifc' => 'audio/x-aiff',
3840
            'aiff' => 'audio/x-aiff',
3841
            'ram' => 'audio/x-pn-realaudio',
3842
            'rm' => 'audio/x-pn-realaudio',
3843
            'rpm' => 'audio/x-pn-realaudio-plugin',
3844
            'ra' => 'audio/x-realaudio',
3845
            'wav' => 'audio/x-wav',
3846
            'bmp' => 'image/bmp',
3847
            'gif' => 'image/gif',
3848
            'jpeg' => 'image/jpeg',
3849
            'jpe' => 'image/jpeg',
3850
            'jpg' => 'image/jpeg',
3851
            'png' => 'image/png',
3852
            'tiff' => 'image/tiff',
3853
            'tif' => 'image/tiff',
3854
            'eml' => 'message/rfc822',
3855
            'css' => 'text/css',
3856
            'html' => 'text/html',
3857
            'htm' => 'text/html',
3858
            'shtml' => 'text/html',
3859
            'log' => 'text/plain',
3860
            'text' => 'text/plain',
3861
            'txt' => 'text/plain',
3862
            'rtx' => 'text/richtext',
3863
            'rtf' => 'text/rtf',
3864
            'vcf' => 'text/vcard',
3865
            'vcard' => 'text/vcard',
3866
            'ics' => 'text/calendar',
3867
            'xml' => 'text/xml',
3868
            'xsl' => 'text/xml',
3869
            'mpeg' => 'video/mpeg',
3870
            'mpe' => 'video/mpeg',
3871
            'mpg' => 'video/mpeg',
3872
            'mov' => 'video/quicktime',
3873
            'qt' => 'video/quicktime',
3874
            'rv' => 'video/vnd.rn-realvideo',
3875
            'avi' => 'video/x-msvideo',
3876
            'movie' => 'video/x-sgi-movie',
3877
        ];
3878
        if (array_key_exists(strtolower($ext), $mimes)) {
3879
            return $mimes[strtolower($ext)];
3880
        }
3881
3882
        return 'application/octet-stream';
3883
    }
3884
3885
    /**
3886
     * Map a file name to a MIME type.
3887
     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
3888
     *
3889
     * @param string $filename A file name or full path, does not need to exist as a file
3890
     *
3891
     * @return string
3892
     */
3893
    public static function filenameToType($filename)
3894
    {
3895
        // In case the path is a URL, strip any query string before getting extension
3896
        $qpos = strpos($filename, '?');
3897
        if (false !== $qpos) {
3898
            $filename = substr($filename, 0, $qpos);
3899
        }
3900
        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
3901
3902
        return static::_mime_types($ext);
0 ignored issues
show
Bug introduced by
It seems like $ext defined by static::mb_pathinfo($filename, PATHINFO_EXTENSION) on line 3900 can also be of type array<string,string>; however, PHPMailer\PHPMailer\PHPMailer::_mime_types() does only seem to accept string, 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...
3903
    }
3904
3905
    /**
3906
     * Multi-byte-safe pathinfo replacement.
3907
     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
3908
     *
3909
     * @see    http://www.php.net/manual/en/function.pathinfo.php#107461
3910
     *
3911
     * @param string     $path    A filename or path, does not need to exist as a file
3912
     * @param int|string $options Either a PATHINFO_* constant,
3913
     *                            or a string name to return only the specified piece
3914
     *
3915
     * @return string|array
3916
     */
3917
    public static function mb_pathinfo($path, $options = null)
3918
    {
3919
        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
3920
        $pathinfo = [];
3921
        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$#im', $path, $pathinfo)) {
3922
            if (array_key_exists(1, $pathinfo)) {
3923
                $ret['dirname'] = $pathinfo[1];
3924
            }
3925
            if (array_key_exists(2, $pathinfo)) {
3926
                $ret['basename'] = $pathinfo[2];
3927
            }
3928
            if (array_key_exists(5, $pathinfo)) {
3929
                $ret['extension'] = $pathinfo[5];
3930
            }
3931
            if (array_key_exists(3, $pathinfo)) {
3932
                $ret['filename'] = $pathinfo[3];
3933
            }
3934
        }
3935
        switch ($options) {
3936
            case PATHINFO_DIRNAME:
3937
            case 'dirname':
3938
                return $ret['dirname'];
3939
            case PATHINFO_BASENAME:
3940
            case 'basename':
3941
                return $ret['basename'];
3942
            case PATHINFO_EXTENSION:
3943
            case 'extension':
3944
                return $ret['extension'];
3945
            case PATHINFO_FILENAME:
3946
            case 'filename':
3947
                return $ret['filename'];
3948
            default:
3949
                return $ret;
3950
        }
3951
    }
3952
3953
    /**
3954
     * Set or reset instance properties.
3955
     * You should avoid this function - it's more verbose, less efficient, more error-prone and
3956
     * harder to debug than setting properties directly.
3957
     * Usage Example:
3958
     * `$mail->set('SMTPSecure', 'tls');`
3959
     *   is the same as:
3960
     * `$mail->SMTPSecure = 'tls';`.
3961
     *
3962
     * @param string $name  The property name to set
3963
     * @param mixed  $value The value to set the property to
3964
     *
3965
     * @return bool
3966
     */
3967
    public function set($name, $value = '')
3968
    {
3969
        if (property_exists($this, $name)) {
3970
            $this->$name = $value;
3971
3972
            return true;
3973
        }
3974
        $this->setError($this->lang('variable_set') . $name);
3975
3976
        return false;
3977
    }
3978
3979
    /**
3980
     * Strip newlines to prevent header injection.
3981
     *
3982
     * @param string $str
3983
     *
3984
     * @return string
3985
     */
3986
    public function secureHeader($str)
3987
    {
3988
        return trim(str_replace(["\r", "\n"], '', $str));
3989
    }
3990
3991
    /**
3992
     * Normalize line breaks in a string.
3993
     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
3994
     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
3995
     *
3996
     * @param string $text
3997
     * @param string $breaktype What kind of line break to use; defaults to static::$LE
3998
     *
3999
     * @return string
4000
     */
4001
    public static function normalizeBreaks($text, $breaktype = null)
4002
    {
4003
        if (null === $breaktype) {
4004
            $breaktype = static::$LE;
4005
        }
4006
        // Normalise to \n
4007
        $text = str_replace(["\r\n", "\r"], "\n", $text);
4008
        // Now convert LE as needed
4009
        if ("\n" !== static::$LE) {
4010
            $text = str_replace("\n", $breaktype, $text);
4011
        }
4012
4013
        return $text;
4014
    }
4015
4016
    /**
4017
     * Return the current line break format string.
4018
     *
4019
     * @return string
4020
     */
4021
    public static function getLE()
4022
    {
4023
        return static::$LE;
4024
    }
4025
4026
    /**
4027
     * Set the line break format string, e.g. "\r\n".
4028
     *
4029
     * @param string $le
4030
     */
4031
    protected static function setLE($le)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $le. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
4032
    {
4033
        static::$LE = $le;
4034
    }
4035
4036
    /**
4037
     * Set the public and private key files and password for S/MIME signing.
4038
     *
4039
     * @param string $cert_filename
4040
     * @param string $key_filename
4041
     * @param string $key_pass            Password for private key
4042
     * @param string $extracerts_filename Optional path to chain certificate
4043
     */
4044
    public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
4045
    {
4046
        $this->sign_cert_file = $cert_filename;
4047
        $this->sign_key_file = $key_filename;
4048
        $this->sign_key_pass = $key_pass;
4049
        $this->sign_extracerts_file = $extracerts_filename;
4050
    }
4051
4052
    /**
4053
     * Quoted-Printable-encode a DKIM header.
4054
     *
4055
     * @param string $txt
4056
     *
4057
     * @return string
4058
     */
4059
    public function DKIM_QP($txt)
4060
    {
4061
        $line = '';
4062
        $len = strlen($txt);
4063
        for ($i = 0; $i < $len; ++$i) {
4064
            $ord = ord($txt[$i]);
4065
            if (((0x21 <= $ord) and ($ord <= 0x3A)) or $ord == 0x3C or ((0x3E <= $ord) and ($ord <= 0x7E))) {
4066
                $line .= $txt[$i];
4067
            } else {
4068
                $line .= '=' . sprintf('%02X', $ord);
4069
            }
4070
        }
4071
4072
        return $line;
4073
    }
4074
4075
    /**
4076
     * Generate a DKIM signature.
4077
     *
4078
     * @param string $signHeader
4079
     *
4080
     * @throws Exception
4081
     *
4082
     * @return string The DKIM signature value
4083
     */
4084
    public function DKIM_Sign($signHeader)
4085
    {
4086
        if (!defined('PKCS7_TEXT')) {
4087
            if ($this->exceptions) {
4088
                throw new Exception($this->lang('extension_missing') . 'openssl');
4089
            }
4090
4091
            return '';
4092
        }
4093
        $privKeyStr = !empty($this->DKIM_private_string) ?
4094
            $this->DKIM_private_string :
4095
            file_get_contents($this->DKIM_private);
4096
        if ('' != $this->DKIM_passphrase) {
4097
            $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
4098
        } else {
4099
            $privKey = openssl_pkey_get_private($privKeyStr);
4100
        }
4101
        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
4102
            openssl_pkey_free($privKey);
4103
4104
            return base64_encode($signature);
4105
        }
4106
        openssl_pkey_free($privKey);
4107
4108
        return '';
4109
    }
4110
4111
    /**
4112
     * Generate a DKIM canonicalization header.
4113
     * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
4114
     *
4115
     * @see    https://tools.ietf.org/html/rfc6376#section-3.4.2
4116
     *
4117
     * @param string $signHeader Header
4118
     *
4119
     * @return string
4120
     */
4121
    public function DKIM_HeaderC($signHeader)
4122
    {
4123
        //Unfold all header continuation lines
4124
        //Also collapses folded whitespace.
4125
        //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
4126
        //@see https://tools.ietf.org/html/rfc5322#section-2.2
4127
        //That means this may break if you do something daft like put vertical tabs in your headers.
4128
        $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
4129
        $lines = explode("\r\n", $signHeader);
4130
        foreach ($lines as $key => $line) {
4131
            //If the header is missing a :, skip it as it's invalid
4132
            //This is likely to happen because the explode() above will also split
4133
            //on the trailing LE, leaving an empty line
4134
            if (strpos($line, ':') === false) {
4135
                continue;
4136
            }
4137
            list($heading, $value) = explode(':', $line, 2);
4138
            //Lower-case header name
4139
            $heading = strtolower($heading);
4140
            //Collapse white space within the value
4141
            $value = preg_replace('/[ \t]{2,}/', ' ', $value);
4142
            //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
4143
            //But then says to delete space before and after the colon.
4144
            //Net result is the same as trimming both ends of the value.
4145
            //by elimination, the same applies to the field name
4146
            $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
4147
        }
4148
4149
        return implode(static::$LE, $lines);
4150
    }
4151
4152
    /**
4153
     * Generate a DKIM canonicalization body.
4154
     * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
4155
     *
4156
     * @see    https://tools.ietf.org/html/rfc6376#section-3.4.3
4157
     *
4158
     * @param string $body Message Body
4159
     *
4160
     * @return string
4161
     */
4162
    public function DKIM_BodyC($body)
4163
    {
4164
        if (empty($body)) {
4165
            return static::$LE;
4166
        }
4167
        // Normalize line endings
4168
        $body = static::normalizeBreaks($body);
4169
4170
        //Reduce multiple trailing line breaks to a single one
4171
        return rtrim($body, "\r\n") . static::$LE;
4172
    }
4173
4174
    /**
4175
     * Create the DKIM header and body in a new message header.
4176
     *
4177
     * @param string $headers_line Header lines
4178
     * @param string $subject      Subject
4179
     * @param string $body         Body
4180
     *
4181
     * @return string
4182
     */
4183
    public function DKIM_Add($headers_line, $subject, $body)
4184
    {
4185
        $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms
4186
        $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body
4187
        $DKIMquery = 'dns/txt'; // Query method
4188
        $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone)
4189
        $subject_header = "Subject: $subject";
4190
        $headers = explode(static::$LE, $headers_line);
4191
        $from_header = '';
4192
        $to_header = '';
4193
        $date_header = '';
4194
        $current = '';
4195
        foreach ($headers as $header) {
4196
            if (strpos($header, 'From:') === 0) {
4197
                $from_header = $header;
4198
                $current = 'from_header';
4199
            } elseif (strpos($header, 'To:') === 0) {
4200
                $to_header = $header;
4201
                $current = 'to_header';
4202
            } elseif (strpos($header, 'Date:') === 0) {
4203
                $date_header = $header;
4204
                $current = 'date_header';
4205
            } else {
4206
                if (!empty($$current) and strpos($header, ' =?') === 0) {
4207
                    $$current .= $header;
4208
                } else {
4209
                    $current = '';
4210
                }
4211
            }
4212
        }
4213
        $from = str_replace('|', '=7C', $this->DKIM_QP($from_header));
4214
        $to = str_replace('|', '=7C', $this->DKIM_QP($to_header));
4215
        $date = str_replace('|', '=7C', $this->DKIM_QP($date_header));
4216
        $subject = str_replace(
4217
            '|',
4218
            '=7C',
4219
            $this->DKIM_QP($subject_header)
4220
        ); // Copied header fields (dkim-quoted-printable)
4221
        $body = $this->DKIM_BodyC($body);
4222
        $DKIMlen = strlen($body); // Length of body
4223
        $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body
4224
        if ('' == $this->DKIM_identity) {
4225
            $ident = '';
4226
        } else {
4227
            $ident = ' i=' . $this->DKIM_identity . ';';
4228
        }
4229
        $dkimhdrs = 'DKIM-Signature: v=1; a=' .
4230
            $DKIMsignatureType . '; q=' .
4231
            $DKIMquery . '; l=' .
4232
            $DKIMlen . '; s=' .
4233
            $this->DKIM_selector .
4234
            ";\r\n" .
4235
            "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" .
4236
            "\th=From:To:Date:Subject;\r\n" .
4237
            "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" .
4238
            "\tz=$from\r\n" .
4239
            "\t|$to\r\n" .
4240
            "\t|$date\r\n" .
4241
            "\t|$subject;\r\n" .
4242
            "\tbh=" . $DKIMb64 . ";\r\n" .
4243
            "\tb=";
4244
        $toSign = $this->DKIM_HeaderC(
4245
            $from_header . "\r\n" .
4246
            $to_header . "\r\n" .
4247
            $date_header . "\r\n" .
4248
            $subject_header . "\r\n" .
4249
            $dkimhdrs
4250
        );
4251
        $signed = $this->DKIM_Sign($toSign);
4252
4253
        return static::normalizeBreaks($dkimhdrs . $signed) . static::$LE;
4254
    }
4255
4256
    /**
4257
     * Detect if a string contains a line longer than the maximum line length
4258
     * allowed by RFC 2822 section 2.1.1.
4259
     *
4260
     * @param string $str
4261
     *
4262
     * @return bool
4263
     */
4264
    public static function hasLineLongerThanMax($str)
4265
    {
4266
        return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
4267
    }
4268
4269
    /**
4270
     * Allows for public read access to 'to' property.
4271
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4272
     *
4273
     * @return array
4274
     */
4275
    public function getToAddresses()
4276
    {
4277
        return $this->to;
4278
    }
4279
4280
    /**
4281
     * Allows for public read access to 'cc' property.
4282
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4283
     *
4284
     * @return array
4285
     */
4286
    public function getCcAddresses()
4287
    {
4288
        return $this->cc;
4289
    }
4290
4291
    /**
4292
     * Allows for public read access to 'bcc' property.
4293
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4294
     *
4295
     * @return array
4296
     */
4297
    public function getBccAddresses()
4298
    {
4299
        return $this->bcc;
4300
    }
4301
4302
    /**
4303
     * Allows for public read access to 'ReplyTo' property.
4304
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4305
     *
4306
     * @return array
4307
     */
4308
    public function getReplyToAddresses()
4309
    {
4310
        return $this->ReplyTo;
4311
    }
4312
4313
    /**
4314
     * Allows for public read access to 'all_recipients' property.
4315
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4316
     *
4317
     * @return array
4318
     */
4319
    public function getAllRecipientAddresses()
4320
    {
4321
        return $this->all_recipients;
4322
    }
4323
4324
    /**
4325
     * Perform a callback.
4326
     *
4327
     * @param bool   $isSent
4328
     * @param array  $to
4329
     * @param array  $cc
4330
     * @param array  $bcc
4331
     * @param string $subject
4332
     * @param string $body
4333
     * @param string $from
4334
     */
4335
    protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $cc. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
4336
    {
4337
        if (!empty($this->action_function) and is_callable($this->action_function)) {
4338
            call_user_func_array($this->action_function, [$isSent, $to, $cc, $bcc, $subject, $body, $from]);
4339
        }
4340
    }
4341
4342
    /**
4343
     * Get the OAuth instance.
4344
     *
4345
     * @return OAuth
4346
     */
4347
    public function getOAuth()
4348
    {
4349
        return $this->oauth;
4350
    }
4351
4352
    /**
4353
     * Set an OAuth instance.
4354
     *
4355
     * @param OAuth $oauth
4356
     */
4357
    public function setOAuth(OAuth $oauth)
4358
    {
4359
        $this->oauth = $oauth;
4360
    }
4361
}
4362