Completed
Push — master ( a81350...e6a872 )
by Wanderson
05:44
created

PHPMailer::hasMultiBytes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
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
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
     *   string  $extra         extra information of possible use
477
     *                          "smtp_transaction_id' => last smtp transaction id
478
     *
479
     * @var string
480
     */
481
    public $action_function = '';
482
483
    /**
484
     * What to put in the X-Mailer header.
485
     * Options: An empty string for PHPMailer default, whitespace for none, or a string to use.
486
     *
487
     * @var string
488
     */
489
    public $XMailer = '';
490
491
    /**
492
     * Which validator to use by default when validating email addresses.
493
     * May be a callable to inject your own validator, but there are several built-in validators.
494
     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
495
     *
496
     * @see PHPMailer::validateAddress()
497
     *
498
     * @var string|callable
499
     */
500
    public static $validator = 'php';
501
502
    /**
503
     * An instance of the SMTP sender class.
504
     *
505
     * @var SMTP
506
     */
507
    protected $smtp = null;
508
509
    /**
510
     * The array of 'to' names and addresses.
511
     *
512
     * @var array
513
     */
514
    protected $to = [];
515
516
    /**
517
     * The array of 'cc' names and addresses.
518
     *
519
     * @var array
520
     */
521
    protected $cc = [];
522
523
    /**
524
     * The array of 'bcc' names and addresses.
525
     *
526
     * @var array
527
     */
528
    protected $bcc = [];
529
530
    /**
531
     * The array of reply-to names and addresses.
532
     *
533
     * @var array
534
     */
535
    protected $ReplyTo = [];
536
537
    /**
538
     * An array of all kinds of addresses.
539
     * Includes all of $to, $cc, $bcc.
540
     *
541
     * @see PHPMailer::$to
542
     * @see PHPMailer::$cc
543
     * @see PHPMailer::$bcc
544
     *
545
     * @var array
546
     */
547
    protected $all_recipients = [];
548
549
    /**
550
     * An array of names and addresses queued for validation.
551
     * In send(), valid and non duplicate entries are moved to $all_recipients
552
     * and one of $to, $cc, or $bcc.
553
     * This array is used only for addresses with IDN.
554
     *
555
     * @see PHPMailer::$to
556
     * @see PHPMailer::$cc
557
     * @see PHPMailer::$bcc
558
     * @see PHPMailer::$all_recipients
559
     *
560
     * @var array
561
     */
562
    protected $RecipientsQueue = [];
563
564
    /**
565
     * An array of reply-to names and addresses queued for validation.
566
     * In send(), valid and non duplicate entries are moved to $ReplyTo.
567
     * This array is used only for addresses with IDN.
568
     *
569
     * @see PHPMailer::$ReplyTo
570
     *
571
     * @var array
572
     */
573
    protected $ReplyToQueue = [];
574
575
    /**
576
     * The array of attachments.
577
     *
578
     * @var array
579
     */
580
    protected $attachment = [];
581
582
    /**
583
     * The array of custom headers.
584
     *
585
     * @var array
586
     */
587
    protected $CustomHeader = [];
588
589
    /**
590
     * The most recent Message-ID (including angular brackets).
591
     *
592
     * @var string
593
     */
594
    protected $lastMessageID = '';
595
596
    /**
597
     * The message's MIME type.
598
     *
599
     * @var string
600
     */
601
    protected $message_type = '';
602
603
    /**
604
     * The array of MIME boundary strings.
605
     *
606
     * @var array
607
     */
608
    protected $boundary = [];
609
610
    /**
611
     * The array of available languages.
612
     *
613
     * @var array
614
     */
615
    protected $language = [];
616
617
    /**
618
     * The number of errors encountered.
619
     *
620
     * @var int
621
     */
622
    protected $error_count = 0;
623
624
    /**
625
     * The S/MIME certificate file path.
626
     *
627
     * @var string
628
     */
629
    protected $sign_cert_file = '';
630
631
    /**
632
     * The S/MIME key file path.
633
     *
634
     * @var string
635
     */
636
    protected $sign_key_file = '';
637
638
    /**
639
     * The optional S/MIME extra certificates ("CA Chain") file path.
640
     *
641
     * @var string
642
     */
643
    protected $sign_extracerts_file = '';
644
645
    /**
646
     * The S/MIME password for the key.
647
     * Used only if the key is encrypted.
648
     *
649
     * @var string
650
     */
651
    protected $sign_key_pass = '';
652
653
    /**
654
     * Whether to throw exceptions for errors.
655
     *
656
     * @var bool
657
     */
658
    protected $exceptions = false;
659
660
    /**
661
     * Unique ID used for message ID and boundaries.
662
     *
663
     * @var string
664
     */
665
    protected $uniqueid = '';
666
667
    /**
668
     * The PHPMailer Version number.
669
     *
670
     * @var string
671
     */
672
    const VERSION = '6.0.1';
673
674
    /**
675
     * Error severity: message only, continue processing.
676
     *
677
     * @var int
678
     */
679
    const STOP_MESSAGE = 0;
680
681
    /**
682
     * Error severity: message, likely ok to continue processing.
683
     *
684
     * @var int
685
     */
686
    const STOP_CONTINUE = 1;
687
688
    /**
689
     * Error severity: message, plus full stop, critical error reached.
690
     *
691
     * @var int
692
     */
693
    const STOP_CRITICAL = 2;
694
695
    /**
696
     * SMTP RFC standard line ending.
697
     *
698
     * @var string
699
     */
700
    protected static $LE = "\r\n";
701
702
    /**
703
     * The maximum line length allowed by RFC 2822 section 2.1.1.
704
     *
705
     * @var int
706
     */
707
    const MAX_LINE_LENGTH = 998;
708
709
    /**
710
     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
711
     *
712
     * @var int
713
     */
714
    const STD_LINE_LENGTH = 78;
715
716
    /**
717
     * Constructor.
718
     *
719
     * @param bool $exceptions Should we throw external exceptions?
720
     */
721
    public function __construct($exceptions = null)
722
    {
723
        if (null !== $exceptions) {
724
            $this->exceptions = (bool) $exceptions;
725
        }
726
        //Pick an appropriate debug output format automatically
727
        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
728
    }
729
730
    /**
731
     * Destructor.
732
     */
733
    public function __destruct()
734
    {
735
        //Close any open SMTP connection nicely
736
        $this->smtpClose();
737
    }
738
739
    /**
740
     * Call mail() in a safe_mode-aware fashion.
741
     * Also, unless sendmail_path points to sendmail (or something that
742
     * claims to be sendmail), don't pass params (not a perfect fix,
743
     * but it will do).
744
     *
745
     * @param string      $to      To
746
     * @param string      $subject Subject
747
     * @param string      $body    Message Body
748
     * @param string      $header  Additional Header(s)
749
     * @param string|null $params  Params
750
     *
751
     * @return bool
752
     */
753
    private function mailPassthru($to, $subject, $body, $header, $params)
754
    {
755
        //Check overloading of mail function to avoid double-encoding
756
        if (ini_get('mbstring.func_overload') & 1) {
757
            $subject = $this->secureHeader($subject);
758
        } else {
759
            $subject = $this->encodeHeader($this->secureHeader($subject));
760
        }
761
        //Calling mail() with null params breaks
762
        if (!$this->UseSendmailOptions or null === $params) {
763
            $result = @mail($to, $subject, $body, $header);
764
        } else {
765
            $result = @mail($to, $subject, $body, $header, $params);
766
        }
767
768
        return $result;
769
    }
770
771
    /**
772
     * Output debugging info via user-defined method.
773
     * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug).
774
     *
775
     * @see PHPMailer::$Debugoutput
776
     * @see PHPMailer::$SMTPDebug
777
     *
778
     * @param string $str
779
     */
780
    protected function edebug($str)
781
    {
782
        if ($this->SMTPDebug <= 0) {
783
            return;
784
        }
785
        //Is this a PSR-3 logger?
786
        if (is_a($this->Debugoutput, 'Psr\Log\LoggerInterface')) {
787
            $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...
788
789
            return;
790
        }
791
        //Avoid clash with built-in function names
792 View Code Duplication
        if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
793
            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
794
795
            return;
796
        }
797 View Code Duplication
        switch ($this->Debugoutput) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
798
            case 'error_log':
799
                //Don't output, just log
800
                error_log($str);
801
                break;
802
            case 'html':
803
                //Cleans up output a bit for a better looking, HTML-safe output
804
                echo htmlentities(
805
                    preg_replace('/[\r\n]+/', '', $str),
806
                    ENT_QUOTES,
807
                    'UTF-8'
808
                ), "<br>\n";
809
                break;
810
            case 'echo':
811
            default:
812
                //Normalize line breaks
813
                $str = preg_replace('/\r\n|\r/ms', "\n", $str);
814
                echo gmdate('Y-m-d H:i:s'),
815
                "\t",
816
                    //Trim trailing space
817
                trim(
818
                //Indent for readability, except for trailing break
819
                    str_replace(
820
                        "\n",
821
                        "\n                   \t                  ",
822
                        trim($str)
823
                    )
824
                ),
825
                "\n";
826
        }
827
    }
828
829
    /**
830
     * Sets message type to HTML or plain.
831
     *
832
     * @param bool $isHtml True for HTML mode
833
     */
834
    public function isHTML($isHtml = true)
835
    {
836
        if ($isHtml) {
837
            $this->ContentType = 'text/html';
838
        } else {
839
            $this->ContentType = 'text/plain';
840
        }
841
    }
842
843
    /**
844
     * Send messages using SMTP.
845
     */
846
    public function isSMTP()
847
    {
848
        $this->Mailer = 'smtp';
849
    }
850
851
    /**
852
     * Send messages using PHP's mail() function.
853
     */
854
    public function isMail()
855
    {
856
        $this->Mailer = 'mail';
857
    }
858
859
    /**
860
     * Send messages using $Sendmail.
861
     */
862 View Code Duplication
    public function isSendmail()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
863
    {
864
        $ini_sendmail_path = ini_get('sendmail_path');
865
866
        if (!stristr($ini_sendmail_path, 'sendmail')) {
867
            $this->Sendmail = '/usr/sbin/sendmail';
868
        } else {
869
            $this->Sendmail = $ini_sendmail_path;
870
        }
871
        $this->Mailer = 'sendmail';
872
    }
873
874
    /**
875
     * Send messages using qmail.
876
     */
877 View Code Duplication
    public function isQmail()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
878
    {
879
        $ini_sendmail_path = ini_get('sendmail_path');
880
881
        if (!stristr($ini_sendmail_path, 'qmail')) {
882
            $this->Sendmail = '/var/qmail/bin/qmail-inject';
883
        } else {
884
            $this->Sendmail = $ini_sendmail_path;
885
        }
886
        $this->Mailer = 'qmail';
887
    }
888
889
    /**
890
     * Add a "To" address.
891
     *
892
     * @param string $address The email address to send to
893
     * @param string $name
894
     *
895
     * @return bool true on success, false if address already used or invalid in some way
896
     */
897
    public function addAddress($address, $name = '')
898
    {
899
        return $this->addOrEnqueueAnAddress('to', $address, $name);
900
    }
901
902
    /**
903
     * Add a "CC" address.
904
     *
905
     * @param string $address The email address to send to
906
     * @param string $name
907
     *
908
     * @return bool true on success, false if address already used or invalid in some way
909
     */
910
    public function addCC($address, $name = '')
911
    {
912
        return $this->addOrEnqueueAnAddress('cc', $address, $name);
913
    }
914
915
    /**
916
     * Add a "BCC" address.
917
     *
918
     * @param string $address The email address to send to
919
     * @param string $name
920
     *
921
     * @return bool true on success, false if address already used or invalid in some way
922
     */
923
    public function addBCC($address, $name = '')
924
    {
925
        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
926
    }
927
928
    /**
929
     * Add a "Reply-To" address.
930
     *
931
     * @param string $address The email address to reply to
932
     * @param string $name
933
     *
934
     * @return bool true on success, false if address already used or invalid in some way
935
     */
936
    public function addReplyTo($address, $name = '')
937
    {
938
        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
939
    }
940
941
    /**
942
     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
943
     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
944
     * be modified after calling this function), addition of such addresses is delayed until send().
945
     * Addresses that have been added already return false, but do not throw exceptions.
946
     *
947
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
948
     * @param string $address The email address to send, resp. to reply to
949
     * @param string $name
950
     *
951
     * @throws Exception
952
     *
953
     * @return bool true on success, false if address already used or invalid in some way
954
     */
955
    protected function addOrEnqueueAnAddress($kind, $address, $name)
956
    {
957
        $address = trim($address);
958
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
959
        $pos = strrpos($address, '@');
960 View Code Duplication
        if (false === $pos) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
961
            // At-sign is missing.
962
            $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address";
963
            $this->setError($error_message);
964
            $this->edebug($error_message);
965
            if ($this->exceptions) {
966
                throw new Exception($error_message);
967
            }
968
969
            return false;
970
        }
971
        $params = [$kind, $address, $name];
972
        // Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
973
        if ($this->has8bitChars(substr($address, ++$pos)) and $this->idnSupported()) {
974
            if ('Reply-To' != $kind) {
975
                if (!array_key_exists($address, $this->RecipientsQueue)) {
976
                    $this->RecipientsQueue[$address] = $params;
977
978
                    return true;
979
                }
980
            } else {
981
                if (!array_key_exists($address, $this->ReplyToQueue)) {
982
                    $this->ReplyToQueue[$address] = $params;
983
984
                    return true;
985
                }
986
            }
987
988
            return false;
989
        }
990
991
        // Immediately add standard addresses without IDN.
992
        return call_user_func_array([$this, 'addAnAddress'], $params);
993
    }
994
995
    /**
996
     * Add an address to one of the recipient arrays or to the ReplyTo array.
997
     * Addresses that have been added already return false, but do not throw exceptions.
998
     *
999
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1000
     * @param string $address The email address to send, resp. to reply to
1001
     * @param string $name
1002
     *
1003
     * @throws Exception
1004
     *
1005
     * @return bool true on success, false if address already used or invalid in some way
1006
     */
1007
    protected function addAnAddress($kind, $address, $name = '')
1008
    {
1009
        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1010
            $error_message = $this->lang('Invalid recipient kind: ') . $kind;
1011
            $this->setError($error_message);
1012
            $this->edebug($error_message);
1013
            if ($this->exceptions) {
1014
                throw new Exception($error_message);
1015
            }
1016
1017
            return false;
1018
        }
1019 View Code Duplication
        if (!static::validateAddress($address)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
1020
            $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address";
1021
            $this->setError($error_message);
1022
            $this->edebug($error_message);
1023
            if ($this->exceptions) {
1024
                throw new Exception($error_message);
1025
            }
1026
1027
            return false;
1028
        }
1029
        if ('Reply-To' != $kind) {
1030 View Code Duplication
            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
1031
                array_push($this->$kind, [$address, $name]);
1032
                $this->all_recipients[strtolower($address)] = true;
1033
1034
                return true;
1035
            }
1036
        } else {
1037 View Code Duplication
            if (!array_key_exists(strtolower($address), $this->ReplyTo)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
1038
                $this->ReplyTo[strtolower($address)] = [$address, $name];
1039
1040
                return true;
1041
            }
1042
        }
1043
1044
        return false;
1045
    }
1046
1047
    /**
1048
     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1049
     * of the form "display name <address>" into an array of name/address pairs.
1050
     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1051
     * Note that quotes in the name part are removed.
1052
     *
1053
     * @see    http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1054
     *
1055
     * @param string $addrstr The address list string
1056
     * @param bool   $useimap Whether to use the IMAP extension to parse the list
1057
     *
1058
     * @return array
1059
     */
1060
    public static function parseAddresses($addrstr, $useimap = true)
1061
    {
1062
        $addresses = [];
1063
        if ($useimap and function_exists('imap_rfc822_parse_adrlist')) {
1064
            //Use this built-in parser if it's available
1065
            $list = imap_rfc822_parse_adrlist($addrstr, '');
1066
            foreach ($list as $address) {
1067
                if ('.SYNTAX-ERROR.' != $address->host) {
1068
                    if (static::validateAddress($address->mailbox . '@' . $address->host)) {
1069
                        $addresses[] = [
1070
                            'name' => (property_exists($address, 'personal') ? $address->personal : ''),
1071
                            'address' => $address->mailbox . '@' . $address->host,
1072
                        ];
1073
                    }
1074
                }
1075
            }
1076
        } else {
1077
            //Use this simpler parser
1078
            $list = explode(',', $addrstr);
1079
            foreach ($list as $address) {
1080
                $address = trim($address);
1081
                //Is there a separate name part?
1082
                if (strpos($address, '<') === false) {
1083
                    //No separate name, just use the whole thing
1084
                    if (static::validateAddress($address)) {
1085
                        $addresses[] = [
1086
                            'name' => '',
1087
                            'address' => $address,
1088
                        ];
1089
                    }
1090
                } else {
1091
                    list($name, $email) = explode('<', $address);
1092
                    $email = trim(str_replace('>', '', $email));
1093
                    if (static::validateAddress($email)) {
1094
                        $addresses[] = [
1095
                            'name' => trim(str_replace(['"', "'"], '', $name)),
1096
                            'address' => $email,
1097
                        ];
1098
                    }
1099
                }
1100
            }
1101
        }
1102
1103
        return $addresses;
1104
    }
1105
1106
    /**
1107
     * Set the From and FromName properties.
1108
     *
1109
     * @param string $address
1110
     * @param string $name
1111
     * @param bool   $auto    Whether to also set the Sender address, defaults to true
1112
     *
1113
     * @throws Exception
1114
     *
1115
     * @return bool
1116
     */
1117
    public function setFrom($address, $name = '', $auto = true)
1118
    {
1119
        $address = trim($address);
1120
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1121
        // Don't validate now addresses with IDN. Will be done in send().
1122
        $pos = strrpos($address, '@');
1123
        if (false === $pos or
1124
            (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and
1125
            !static::validateAddress($address)) {
1126
            $error_message = $this->lang('invalid_address') . " (setFrom) $address";
1127
            $this->setError($error_message);
1128
            $this->edebug($error_message);
1129
            if ($this->exceptions) {
1130
                throw new Exception($error_message);
1131
            }
1132
1133
            return false;
1134
        }
1135
        $this->From = $address;
1136
        $this->FromName = $name;
1137
        if ($auto) {
1138
            if (empty($this->Sender)) {
1139
                $this->Sender = $address;
1140
            }
1141
        }
1142
1143
        return true;
1144
    }
1145
1146
    /**
1147
     * Return the Message-ID header of the last email.
1148
     * Technically this is the value from the last time the headers were created,
1149
     * but it's also the message ID of the last sent message except in
1150
     * pathological cases.
1151
     *
1152
     * @return string
1153
     */
1154
    public function getLastMessageID()
1155
    {
1156
        return $this->lastMessageID;
1157
    }
1158
1159
    /**
1160
     * Check that a string looks like an email address.
1161
     * Validation patterns supported:
1162
     * * `auto` Pick best pattern automatically;
1163
     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
1164
     * * `pcre` Use old PCRE implementation;
1165
     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
1166
     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
1167
     * * `noregex` Don't use a regex: super fast, really dumb.
1168
     * Alternatively you may pass in a callable to inject your own validator, for example:
1169
     *
1170
     * ```php
1171
     * PHPMailer::validateAddress('[email protected]', function($address) {
1172
     *     return (strpos($address, '@') !== false);
1173
     * });
1174
     * ```
1175
     *
1176
     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
1177
     *
1178
     * @param string          $address       The email address to check
1179
     * @param string|callable $patternselect Which pattern to use
1180
     *
1181
     * @return bool
1182
     */
1183
    public static function validateAddress($address, $patternselect = null)
1184
    {
1185
        if (null === $patternselect) {
1186
            $patternselect = static::$validator;
1187
        }
1188
        if (is_callable($patternselect)) {
1189
            return call_user_func($patternselect, $address);
1190
        }
1191
        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
1192
        if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) {
1193
            return false;
1194
        }
1195
        switch ($patternselect) {
1196
            case 'pcre': //Kept for BC
1197
            case 'pcre8':
1198
                /*
1199
                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
1200
                 * is based.
1201
                 * In addition to the addresses allowed by filter_var, also permits:
1202
                 *  * dotless domains: `a@b`
1203
                 *  * comments: `1234 @ local(blah) .machine .example`
1204
                 *  * quoted elements: `'"test blah"@example.org'`
1205
                 *  * numeric TLDs: `[email protected]`
1206
                 *  * unbracketed IPv4 literals: `[email protected]`
1207
                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'
1208
                 * Not all of these will necessarily work for sending!
1209
                 *
1210
                 * @see       http://squiloople.com/2009/12/20/email-address-validation/
1211
                 * @copyright 2009-2010 Michael Rushton
1212
                 * Feel free to use and redistribute this code. But please keep this copyright notice.
1213
                 */
1214
                return (bool) preg_match(
1215
                    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
1216
                    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
1217
                    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
1218
                    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
1219
                    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
1220
                    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
1221
                    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
1222
                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
1223
                    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
1224
                    $address
1225
                );
1226
            case 'html5':
1227
                /*
1228
                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
1229
                 *
1230
                 * @see http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email)
1231
                 */
1232
                return (bool) preg_match(
1233
                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
1234
                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
1235
                    $address
1236
                );
1237
            case 'php':
1238
            default:
1239
                return (bool) filter_var($address, FILTER_VALIDATE_EMAIL);
1240
        }
1241
    }
1242
1243
    /**
1244
     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
1245
     * `intl` and `mbstring` PHP extensions.
1246
     *
1247
     * @return bool `true` if required functions for IDN support are present
1248
     */
1249
    public function idnSupported()
1250
    {
1251
        return function_exists('idn_to_ascii') and function_exists('mb_convert_encoding');
1252
    }
1253
1254
    /**
1255
     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
1256
     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
1257
     * This function silently returns unmodified address if:
1258
     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
1259
     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
1260
     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).
1261
     *
1262
     * @see    PHPMailer::$CharSet
1263
     *
1264
     * @param string $address The email address to convert
1265
     *
1266
     * @return string The encoded address in ASCII form
1267
     */
1268
    public function punyencodeAddress($address)
1269
    {
1270
        // Verify we have required functions, CharSet, and at-sign.
1271
        $pos = strrpos($address, '@');
1272
        if ($this->idnSupported() and
1273
            !empty($this->CharSet) and
1274
            false !== $pos
1275
        ) {
1276
            $domain = substr($address, ++$pos);
1277
            // Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
1278
            if ($this->has8bitChars($domain) and @mb_check_encoding($domain, $this->CharSet)) {
1279
                $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet);
1280
                //Ignore IDE complaints about this line - method signature changed in PHP 5.4
1281
                $errorcode = 0;
1282
                $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46);
1283
                if (false !== $punycode) {
1284
                    return substr($address, 0, $pos) . $punycode;
1285
                }
1286
            }
1287
        }
1288
1289
        return $address;
1290
    }
1291
1292
    /**
1293
     * Create a message and send it.
1294
     * Uses the sending method specified by $Mailer.
1295
     *
1296
     * @throws Exception
1297
     *
1298
     * @return bool false on error - See the ErrorInfo property for details of the error
1299
     */
1300
    public function send()
1301
    {
1302
        try {
1303
            if (!$this->preSend()) {
1304
                return false;
1305
            }
1306
1307
            return $this->postSend();
1308
        } catch (Exception $exc) {
1309
            $this->mailHeader = '';
1310
            $this->setError($exc->getMessage());
1311
            if ($this->exceptions) {
1312
                throw $exc;
1313
            }
1314
1315
            return false;
1316
        }
1317
    }
1318
1319
    /**
1320
     * Prepare a message for sending.
1321
     *
1322
     * @throws Exception
1323
     *
1324
     * @return bool
1325
     */
1326
    public function preSend()
1327
    {
1328
        if ('smtp' == $this->Mailer or
1329
            ('mail' == $this->Mailer and stripos(PHP_OS, 'WIN') === 0)
1330
        ) {
1331
            //SMTP mandates RFC-compliant line endings
1332
            //and it's also used with mail() on Windows
1333
            static::setLE("\r\n");
1334
        } else {
1335
            //Maintain backward compatibility with legacy Linux command line mailers
1336
            static::setLE(PHP_EOL);
1337
        }
1338
        //Check for buggy PHP versions that add a header with an incorrect line break
1339
        if (ini_get('mail.add_x_header') == 1
1340
            and 'mail' == $this->Mailer
1341
            and stripos(PHP_OS, 'WIN') === 0
1342
            and ((version_compare(PHP_VERSION, '7.0.0', '>=')
1343
                    and version_compare(PHP_VERSION, '7.0.17', '<'))
1344
                or (version_compare(PHP_VERSION, '7.1.0', '>=')
1345
                    and version_compare(PHP_VERSION, '7.1.3', '<')))
1346
        ) {
1347
            trigger_error(
1348
                'Your version of PHP is affected by a bug that may result in corrupted messages.' .
1349
                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
1350
                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
1351
                E_USER_WARNING
1352
            );
1353
        }
1354
1355
        try {
1356
            $this->error_count = 0; // Reset errors
1357
            $this->mailHeader = '';
1358
1359
            // Dequeue recipient and Reply-To addresses with IDN
1360
            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
1361
                $params[1] = $this->punyencodeAddress($params[1]);
1362
                call_user_func_array([$this, 'addAnAddress'], $params);
1363
            }
1364
            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
1365
                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
1366
            }
1367
1368
            // Validate From, Sender, and ConfirmReadingTo addresses
1369
            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
1370
                $this->$address_kind = trim($this->$address_kind);
1371
                if (empty($this->$address_kind)) {
1372
                    continue;
1373
                }
1374
                $this->$address_kind = $this->punyencodeAddress($this->$address_kind);
1375
                if (!static::validateAddress($this->$address_kind)) {
1376
                    $error_message = $this->lang('invalid_address') . ' (punyEncode) ' . $this->$address_kind;
1377
                    $this->setError($error_message);
1378
                    $this->edebug($error_message);
1379
                    if ($this->exceptions) {
1380
                        throw new Exception($error_message);
1381
                    }
1382
1383
                    return false;
1384
                }
1385
            }
1386
1387
            // Set whether the message is multipart/alternative
1388
            if ($this->alternativeExists()) {
1389
                $this->ContentType = 'multipart/alternative';
1390
            }
1391
1392
            $this->setMessageType();
1393
            // Refuse to send an empty message unless we are specifically allowing it
1394
            if (!$this->AllowEmpty and empty($this->Body)) {
1395
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
1396
            }
1397
1398
            //Trim subject consistently
1399
            $this->Subject = trim($this->Subject);
1400
            // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
1401
            $this->MIMEHeader = '';
1402
            $this->MIMEBody = $this->createBody();
1403
            // createBody may have added some headers, so retain them
1404
            $tempheaders = $this->MIMEHeader;
1405
            $this->MIMEHeader = $this->createHeader();
1406
            $this->MIMEHeader .= $tempheaders;
1407
1408
            // To capture the complete message when using mail(), create
1409
            // an extra header list which createHeader() doesn't fold in
1410
            if ('mail' == $this->Mailer) {
1411
                if (count($this->to) > 0) {
1412
                    $this->mailHeader .= $this->addrAppend('To', $this->to);
1413
                } else {
1414
                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
1415
                }
1416
                $this->mailHeader .= $this->headerLine(
1417
                    'Subject',
1418
                    $this->encodeHeader($this->secureHeader($this->Subject))
1419
                );
1420
            }
1421
1422
            // Sign with DKIM if enabled
1423
            if (!empty($this->DKIM_domain)
1424
                and !empty($this->DKIM_selector)
1425
                and (!empty($this->DKIM_private_string)
1426
                    or (!empty($this->DKIM_private) and file_exists($this->DKIM_private))
1427
                )
1428
            ) {
1429
                $header_dkim = $this->DKIM_Add(
1430
                    $this->MIMEHeader . $this->mailHeader,
1431
                    $this->encodeHeader($this->secureHeader($this->Subject)),
1432
                    $this->MIMEBody
1433
                );
1434
                $this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . static::$LE .
1435
                    static::normalizeBreaks($header_dkim) . static::$LE;
1436
            }
1437
1438
            return true;
1439
        } catch (Exception $exc) {
1440
            $this->setError($exc->getMessage());
1441
            if ($this->exceptions) {
1442
                throw $exc;
1443
            }
1444
1445
            return false;
1446
        }
1447
    }
1448
1449
    /**
1450
     * Actually send a message via the selected mechanism.
1451
     *
1452
     * @throws Exception
1453
     *
1454
     * @return bool
1455
     */
1456
    public function postSend()
1457
    {
1458
        try {
1459
            // Choose the mailer and send through it
1460
            switch ($this->Mailer) {
1461
                case 'sendmail':
1462
                case 'qmail':
1463
                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
1464
                case 'smtp':
1465
                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
1466
                case 'mail':
1467
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1468
                default:
1469
                    $sendMethod = $this->Mailer . 'Send';
1470
                    if (method_exists($this, $sendMethod)) {
1471
                        return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody);
1472
                    }
1473
1474
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1475
            }
1476
        } catch (Exception $exc) {
1477
            $this->setError($exc->getMessage());
1478
            $this->edebug($exc->getMessage());
1479
            if ($this->exceptions) {
1480
                throw $exc;
1481
            }
1482
        }
1483
1484
        return false;
1485
    }
1486
1487
    /**
1488
     * Send mail using the $Sendmail program.
1489
     *
1490
     * @see    PHPMailer::$Sendmail
1491
     *
1492
     * @param string $header The message headers
1493
     * @param string $body   The message body
1494
     *
1495
     * @throws Exception
1496
     *
1497
     * @return bool
1498
     */
1499
    protected function sendmailSend($header, $body)
1500
    {
1501
        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1502
        if (!empty($this->Sender) and self::isShellSafe($this->Sender)) {
1503
            if ('qmail' == $this->Mailer) {
1504
                $sendmailFmt = '%s -f%s';
1505
            } else {
1506
                $sendmailFmt = '%s -oi -f%s -t';
1507
            }
1508
        } else {
1509
            if ('qmail' == $this->Mailer) {
1510
                $sendmailFmt = '%s';
1511
            } else {
1512
                $sendmailFmt = '%s -oi -t';
1513
            }
1514
        }
1515
1516
        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
1517
1518
        if ($this->SingleTo) {
1519
            foreach ($this->SingleToArray as $toAddr) {
1520
                $mail = @popen($sendmail, 'w');
1521
                if (!$mail) {
1522
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1523
                }
1524
                fwrite($mail, 'To: ' . $toAddr . "\n");
1525
                fwrite($mail, $header);
1526
                fwrite($mail, $body);
1527
                $result = pclose($mail);
1528
                $this->doCallback(
1529
                    ($result == 0),
1530
                    [$toAddr],
1531
                    $this->cc,
1532
                    $this->bcc,
1533
                    $this->Subject,
1534
                    $body,
1535
                    $this->From,
1536
                    []
1537
                );
1538
                if (0 !== $result) {
1539
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1540
                }
1541
            }
1542
        } else {
1543
            $mail = @popen($sendmail, 'w');
1544
            if (!$mail) {
1545
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1546
            }
1547
            fwrite($mail, $header);
1548
            fwrite($mail, $body);
1549
            $result = pclose($mail);
1550
            $this->doCallback(
1551
                ($result == 0),
1552
                $this->to,
1553
                $this->cc,
1554
                $this->bcc,
1555
                $this->Subject,
1556
                $body,
1557
                $this->From,
1558
                []
1559
            );
1560
            if (0 !== $result) {
1561
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1562
            }
1563
        }
1564
1565
        return true;
1566
    }
1567
1568
    /**
1569
     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
1570
     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
1571
     *
1572
     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
1573
     *
1574
     * @param string $string The string to be validated
1575
     *
1576
     * @return bool
1577
     */
1578
    protected static function isShellSafe($string)
1579
    {
1580
        // Future-proof
1581
        if (escapeshellcmd($string) !== $string
1582
            or !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
1583
        ) {
1584
            return false;
1585
        }
1586
1587
        $length = strlen($string);
1588
1589
        for ($i = 0; $i < $length; ++$i) {
1590
            $c = $string[$i];
1591
1592
            // All other characters have a special meaning in at least one common shell, including = and +.
1593
            // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
1594
            // Note that this does permit non-Latin alphanumeric characters based on the current locale.
1595
            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
1596
                return false;
1597
            }
1598
        }
1599
1600
        return true;
1601
    }
1602
1603
    /**
1604
     * Send mail using the PHP mail() function.
1605
     *
1606
     * @see    http://www.php.net/manual/en/book.mail.php
1607
     *
1608
     * @param string $header The message headers
1609
     * @param string $body   The message body
1610
     *
1611
     * @throws Exception
1612
     *
1613
     * @return bool
1614
     */
1615
    protected function mailSend($header, $body)
1616
    {
1617
        $toArr = [];
1618
        foreach ($this->to as $toaddr) {
1619
            $toArr[] = $this->addrFormat($toaddr);
1620
        }
1621
        $to = implode(', ', $toArr);
1622
1623
        $params = null;
1624
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1625
        if (!empty($this->Sender) and static::validateAddress($this->Sender)) {
1626
            //A space after `-f` is optional, but there is a long history of its presence
1627
            //causing problems, so we don't use one
1628
            //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1629
            //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
1630
            //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
1631
            //Example problem: https://www.drupal.org/node/1057954
1632
            // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1633
            if (self::isShellSafe($this->Sender)) {
1634
                $params = sprintf('-f%s', $this->Sender);
1635
            }
1636
        }
1637
        if (!empty($this->Sender) and static::validateAddress($this->Sender)) {
1638
            $old_from = ini_get('sendmail_from');
1639
            ini_set('sendmail_from', $this->Sender);
1640
        }
1641
        $result = false;
1642
        if ($this->SingleTo and count($toArr) > 1) {
1643
            foreach ($toArr as $toAddr) {
1644
                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
1645
                $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
1646
            }
1647
        } else {
1648
            $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
1649
            $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
1650
        }
1651
        if (isset($old_from)) {
1652
            ini_set('sendmail_from', $old_from);
1653
        }
1654
        if (!$result) {
1655
            throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
1656
        }
1657
1658
        return true;
1659
    }
1660
1661
    /**
1662
     * Get an instance to use for SMTP operations.
1663
     * Override this function to load your own SMTP implementation,
1664
     * or set one with setSMTPInstance.
1665
     *
1666
     * @return SMTP
1667
     */
1668
    public function getSMTPInstance()
1669
    {
1670
        if (!is_object($this->smtp)) {
1671
            $this->smtp = new SMTP();
1672
        }
1673
1674
        return $this->smtp;
1675
    }
1676
1677
    /**
1678
     * Provide an instance to use for SMTP operations.
1679
     *
1680
     * @param SMTP $smtp
1681
     *
1682
     * @return SMTP
1683
     */
1684
    public function setSMTPInstance(SMTP $smtp)
1685
    {
1686
        $this->smtp = $smtp;
1687
1688
        return $this->smtp;
1689
    }
1690
1691
    /**
1692
     * Send mail via SMTP.
1693
     * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
1694
     *
1695
     * @see PHPMailer::setSMTPInstance() to use a different class.
1696
     *
1697
     * @uses \PHPMailer\PHPMailer\SMTP
1698
     *
1699
     * @param string $header The message headers
1700
     * @param string $body   The message body
1701
     *
1702
     * @throws Exception
1703
     *
1704
     * @return bool
1705
     */
1706
    protected function smtpSend($header, $body)
1707
    {
1708
        $bad_rcpt = [];
1709
        if (!$this->smtpConnect($this->SMTPOptions)) {
1710
            throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
1711
        }
1712
        //Sender already validated in preSend()
1713
        if ('' == $this->Sender) {
1714
            $smtp_from = $this->From;
1715
        } else {
1716
            $smtp_from = $this->Sender;
1717
        }
1718
        if (!$this->smtp->mail($smtp_from)) {
1719
            $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
1720
            throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
1721
        }
1722
1723
        $callbacks = [];
1724
        // Attempt to send to all recipients
1725
        foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
1726
            foreach ($togroup as $to) {
1727
                if (!$this->smtp->recipient($to[0])) {
1728
                    $error = $this->smtp->getError();
1729
                    $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
1730
                    $isSent = false;
1731
                } else {
1732
                    $isSent = true;
1733
                }
1734
1735
                $callbacks[] = ['issent'=>$isSent, 'to'=>$to[0]];
1736
            }
1737
        }
1738
1739
        // Only send the DATA command if we have viable recipients
1740
        if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) {
1741
            throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
1742
        }
1743
1744
        $smtp_transaction_id = $this->smtp->getLastTransactionID();
1745
1746
        if ($this->SMTPKeepAlive) {
1747
            $this->smtp->reset();
1748
        } else {
1749
            $this->smtp->quit();
1750
            $this->smtp->close();
1751
        }
1752
1753
        foreach ($callbacks as $cb) {
1754
            $this->doCallback(
1755
                $cb['issent'],
1756
                [$cb['to']],
1757
                [],
1758
                [],
1759
                $this->Subject,
1760
                $body,
1761
                $this->From,
1762
                ['smtp_transaction_id' => $smtp_transaction_id]
1763
            );
1764
        }
1765
1766
        //Create error message for any bad addresses
1767
        if (count($bad_rcpt) > 0) {
1768
            $errstr = '';
1769
            foreach ($bad_rcpt as $bad) {
1770
                $errstr .= $bad['to'] . ': ' . $bad['error'];
1771
            }
1772
            throw new Exception(
1773
                $this->lang('recipients_failed') . $errstr,
1774
                self::STOP_CONTINUE
1775
            );
1776
        }
1777
1778
        return true;
1779
    }
1780
1781
    /**
1782
     * Initiate a connection to an SMTP server.
1783
     * Returns false if the operation failed.
1784
     *
1785
     * @param array $options An array of options compatible with stream_context_create()
1786
     *
1787
     * @throws Exception
1788
     *
1789
     * @uses \PHPMailer\PHPMailer\SMTP
1790
     *
1791
     * @return bool
1792
     */
1793
    public function smtpConnect($options = null)
1794
    {
1795
        if (null === $this->smtp) {
1796
            $this->smtp = $this->getSMTPInstance();
1797
        }
1798
1799
        //If no options are provided, use whatever is set in the instance
1800
        if (null === $options) {
1801
            $options = $this->SMTPOptions;
1802
        }
1803
1804
        // Already connected?
1805
        if ($this->smtp->connected()) {
1806
            return true;
1807
        }
1808
1809
        $this->smtp->setTimeout($this->Timeout);
1810
        $this->smtp->setDebugLevel($this->SMTPDebug);
1811
        $this->smtp->setDebugOutput($this->Debugoutput);
1812
        $this->smtp->setVerp($this->do_verp);
1813
        $hosts = explode(';', $this->Host);
1814
        $lastexception = null;
1815
1816
        foreach ($hosts as $hostentry) {
1817
            $hostinfo = [];
1818 View Code Duplication
            if (!preg_match(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
1819
                '/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*|\[[a-fA-F0-9:]+\]):?([0-9]*)$/',
1820
                trim($hostentry),
1821
                $hostinfo
1822
            )) {
1823
                static::edebug($this->lang('connect_host') . ' ' . $hostentry);
1824
                // Not a valid host entry
1825
                continue;
1826
            }
1827
            // $hostinfo[2]: optional ssl or tls prefix
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1828
            // $hostinfo[3]: the hostname
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1829
            // $hostinfo[4]: optional port number
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1830
            // The host string prefix can temporarily override the current setting for SMTPSecure
1831
            // If it's not specified, the default value is used
1832
1833
            //Check the host name is a valid name or IP address before trying to use it
1834 View Code Duplication
            if (!static::isValidHost($hostinfo[3])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
1835
                static::edebug($this->lang('connect_host') . ' ' . $hostentry);
1836
                continue;
1837
            }
1838
            $prefix = '';
1839
            $secure = $this->SMTPSecure;
1840
            $tls = ('tls' == $this->SMTPSecure);
1841
            if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) {
1842
                $prefix = 'ssl://';
1843
                $tls = false; // Can't have SSL and TLS at the same time
1844
                $secure = 'ssl';
1845
            } elseif ('tls' == $hostinfo[2]) {
1846
                $tls = true;
1847
                // tls doesn't use a prefix
1848
                $secure = 'tls';
1849
            }
1850
            //Do we need the OpenSSL extension?
1851
            $sslext = defined('OPENSSL_ALGO_SHA256');
1852
            if ('tls' === $secure or 'ssl' === $secure) {
1853
                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
1854
                if (!$sslext) {
1855
                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
1856
                }
1857
            }
1858
            $host = $hostinfo[3];
1859
            $port = $this->Port;
1860
            $tport = (int) $hostinfo[4];
1861
            if ($tport > 0 and $tport < 65536) {
1862
                $port = $tport;
1863
            }
1864
            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
1865
                try {
1866
                    if ($this->Helo) {
1867
                        $hello = $this->Helo;
1868
                    } else {
1869
                        $hello = $this->serverHostname();
1870
                    }
1871
                    $this->smtp->hello($hello);
1872
                    //Automatically enable TLS encryption if:
1873
                    // * it's not disabled
1874
                    // * we have openssl extension
1875
                    // * we are not already using SSL
1876
                    // * the server offers STARTTLS
1877
                    if ($this->SMTPAutoTLS and $sslext and $secure != 'ssl' and $this->smtp->getServerExt('STARTTLS')) {
1878
                        $tls = true;
1879
                    }
1880
                    if ($tls) {
1881
                        if (!$this->smtp->startTLS()) {
1882
                            throw new Exception($this->lang('connect_host'));
1883
                        }
1884
                        // We must resend EHLO after TLS negotiation
1885
                        $this->smtp->hello($hello);
1886
                    }
1887
                    if ($this->SMTPAuth) {
1888
                        if (!$this->smtp->authenticate(
1889
                            $this->Username,
1890
                            $this->Password,
1891
                            $this->AuthType,
1892
                            $this->oauth
1893
                        )
1894
                        ) {
1895
                            throw new Exception($this->lang('authenticate'));
1896
                        }
1897
                    }
1898
1899
                    return true;
1900
                } catch (Exception $exc) {
1901
                    $lastexception = $exc;
1902
                    $this->edebug($exc->getMessage());
1903
                    // We must have connected, but then failed TLS or Auth, so close connection nicely
1904
                    $this->smtp->quit();
1905
                }
1906
            }
1907
        }
1908
        // If we get here, all connection attempts have failed, so close connection hard
1909
        $this->smtp->close();
1910
        // As we've caught all exceptions, just report whatever the last one was
1911
        if ($this->exceptions and null !== $lastexception) {
1912
            throw $lastexception;
1913
        }
1914
1915
        return false;
1916
    }
1917
1918
    /**
1919
     * Close the active SMTP session if one exists.
1920
     */
1921
    public function smtpClose()
1922
    {
1923
        if (null !== $this->smtp) {
1924
            if ($this->smtp->connected()) {
1925
                $this->smtp->quit();
1926
                $this->smtp->close();
1927
            }
1928
        }
1929
    }
1930
1931
    /**
1932
     * Set the language for error messages.
1933
     * Returns false if it cannot load the language file.
1934
     * The default language is English.
1935
     *
1936
     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
1937
     * @param string $lang_path Path to the language file directory, with trailing separator (slash)
1938
     *
1939
     * @return bool
1940
     */
1941
    public function setLanguage($langcode = 'en', $lang_path = '')
1942
    {
1943
        // Backwards compatibility for renamed language codes
1944
        $renamed_langcodes = [
1945
            'br' => 'pt_br',
1946
            'cz' => 'cs',
1947
            'dk' => 'da',
1948
            'no' => 'nb',
1949
            'se' => 'sv',
1950
            'sr' => 'rs',
1951
        ];
1952
1953
        if (isset($renamed_langcodes[$langcode])) {
1954
            $langcode = $renamed_langcodes[$langcode];
1955
        }
1956
1957
        // Define full set of translatable strings in English
1958
        $PHPMAILER_LANG = [
1959
            'authenticate' => 'SMTP Error: Could not authenticate.',
1960
            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
1961
            'data_not_accepted' => 'SMTP Error: data not accepted.',
1962
            'empty_message' => 'Message body empty',
1963
            'encoding' => 'Unknown encoding: ',
1964
            'execute' => 'Could not execute: ',
1965
            'file_access' => 'Could not access file: ',
1966
            'file_open' => 'File Error: Could not open file: ',
1967
            'from_failed' => 'The following From address failed: ',
1968
            'instantiate' => 'Could not instantiate mail function.',
1969
            'invalid_address' => 'Invalid address: ',
1970
            'mailer_not_supported' => ' mailer is not supported.',
1971
            'provide_address' => 'You must provide at least one recipient email address.',
1972
            'recipients_failed' => 'SMTP Error: The following recipients failed: ',
1973
            'signing' => 'Signing Error: ',
1974
            'smtp_connect_failed' => 'SMTP connect() failed.',
1975
            'smtp_error' => 'SMTP server error: ',
1976
            'variable_set' => 'Cannot set or reset variable: ',
1977
            'extension_missing' => 'Extension missing: ',
1978
        ];
1979
        if (empty($lang_path)) {
1980
            // Calculate an absolute path so it can work if CWD is not here
1981
            $lang_path = __DIR__ . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
1982
        }
1983
        //Validate $langcode
1984
        if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) {
1985
            $langcode = 'en';
1986
        }
1987
        $foundlang = true;
1988
        $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
1989
        // There is no English translation file
1990
        if ('en' != $langcode) {
1991
            // Make sure language file path is readable
1992
            if (!file_exists($lang_file)) {
1993
                $foundlang = false;
1994
            } else {
1995
                // Overwrite language-specific strings.
1996
                // This way we'll never have missing translation keys.
1997
                $foundlang = include $lang_file;
1998
            }
1999
        }
2000
        $this->language = $PHPMAILER_LANG;
2001
2002
        return (bool) $foundlang; // Returns false if language not found
2003
    }
2004
2005
    /**
2006
     * Get the array of strings for the current language.
2007
     *
2008
     * @return array
2009
     */
2010
    public function getTranslations()
2011
    {
2012
        return $this->language;
2013
    }
2014
2015
    /**
2016
     * Create recipient headers.
2017
     *
2018
     * @param string $type
2019
     * @param array  $addr An array of recipients,
2020
     *                     where each recipient is a 2-element indexed array with element 0 containing an address
2021
     *                     and element 1 containing a name, like:
2022
     *                     [['[email protected]', 'Joe User'], ['[email protected]', 'Zoe User']]
2023
     *
2024
     * @return string
2025
     */
2026
    public function addrAppend($type, $addr)
2027
    {
2028
        $addresses = [];
2029
        foreach ($addr as $address) {
2030
            $addresses[] = $this->addrFormat($address);
2031
        }
2032
2033
        return $type . ': ' . implode(', ', $addresses) . static::$LE;
2034
    }
2035
2036
    /**
2037
     * Format an address for use in a message header.
2038
     *
2039
     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
2040
     *                    ['[email protected]', 'Joe User']
2041
     *
2042
     * @return string
2043
     */
2044
    public function addrFormat($addr)
2045
    {
2046
        if (empty($addr[1])) { // No name provided
2047
            return $this->secureHeader($addr[0]);
2048
        }
2049
2050
        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader(
2051
                $addr[0]
2052
            ) . '>';
2053
    }
2054
2055
    /**
2056
     * Word-wrap message.
2057
     * For use with mailers that do not automatically perform wrapping
2058
     * and for quoted-printable encoded messages.
2059
     * Original written by philippe.
2060
     *
2061
     * @param string $message The message to wrap
2062
     * @param int    $length  The line length to wrap to
2063
     * @param bool   $qp_mode Whether to run in Quoted-Printable mode
2064
     *
2065
     * @return string
2066
     */
2067
    public function wrapText($message, $length, $qp_mode = false)
2068
    {
2069
        if ($qp_mode) {
2070
            $soft_break = sprintf(' =%s', static::$LE);
2071
        } else {
2072
            $soft_break = static::$LE;
2073
        }
2074
        // If utf-8 encoding is used, we will need to make sure we don't
2075
        // split multibyte characters when we wrap
2076
        $is_utf8 = (strtolower($this->CharSet) == 'utf-8');
2077
        $lelen = strlen(static::$LE);
2078
        $crlflen = strlen(static::$LE);
2079
2080
        $message = static::normalizeBreaks($message);
2081
        //Remove a trailing line break
2082
        if (substr($message, -$lelen) == static::$LE) {
2083
            $message = substr($message, 0, -$lelen);
2084
        }
2085
2086
        //Split message into lines
2087
        $lines = explode(static::$LE, $message);
2088
        //Message will be rebuilt in here
2089
        $message = '';
2090
        foreach ($lines as $line) {
2091
            $words = explode(' ', $line);
2092
            $buf = '';
2093
            $firstword = true;
2094
            foreach ($words as $word) {
2095
                if ($qp_mode and (strlen($word) > $length)) {
2096
                    $space_left = $length - strlen($buf) - $crlflen;
2097
                    if (!$firstword) {
2098
                        if ($space_left > 20) {
2099
                            $len = $space_left;
2100 View Code Duplication
                            if ($is_utf8) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2101
                                $len = $this->utf8CharBoundary($word, $len);
2102
                            } elseif (substr($word, $len - 1, 1) == '=') {
2103
                                --$len;
2104
                            } elseif (substr($word, $len - 2, 1) == '=') {
2105
                                $len -= 2;
2106
                            }
2107
                            $part = substr($word, 0, $len);
2108
                            $word = substr($word, $len);
2109
                            $buf .= ' ' . $part;
2110
                            $message .= $buf . sprintf('=%s', static::$LE);
2111
                        } else {
2112
                            $message .= $buf . $soft_break;
2113
                        }
2114
                        $buf = '';
2115
                    }
2116
                    while (strlen($word) > 0) {
2117
                        if ($length <= 0) {
2118
                            break;
2119
                        }
2120
                        $len = $length;
2121 View Code Duplication
                        if ($is_utf8) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2122
                            $len = $this->utf8CharBoundary($word, $len);
2123
                        } elseif (substr($word, $len - 1, 1) == '=') {
2124
                            --$len;
2125
                        } elseif (substr($word, $len - 2, 1) == '=') {
2126
                            $len -= 2;
2127
                        }
2128
                        $part = substr($word, 0, $len);
2129
                        $word = substr($word, $len);
2130
2131
                        if (strlen($word) > 0) {
2132
                            $message .= $part . sprintf('=%s', static::$LE);
2133
                        } else {
2134
                            $buf = $part;
2135
                        }
2136
                    }
2137
                } else {
2138
                    $buf_o = $buf;
2139
                    if (!$firstword) {
2140
                        $buf .= ' ';
2141
                    }
2142
                    $buf .= $word;
2143
2144
                    if (strlen($buf) > $length and $buf_o != '') {
2145
                        $message .= $buf_o . $soft_break;
2146
                        $buf = $word;
2147
                    }
2148
                }
2149
                $firstword = false;
2150
            }
2151
            $message .= $buf . static::$LE;
2152
        }
2153
2154
        return $message;
2155
    }
2156
2157
    /**
2158
     * Find the last character boundary prior to $maxLength in a utf-8
2159
     * quoted-printable encoded string.
2160
     * Original written by Colin Brown.
2161
     *
2162
     * @param string $encodedText utf-8 QP text
2163
     * @param int    $maxLength   Find the last character boundary prior to this length
2164
     *
2165
     * @return int
2166
     */
2167
    public function utf8CharBoundary($encodedText, $maxLength)
2168
    {
2169
        $foundSplitPos = false;
2170
        $lookBack = 3;
2171
        while (!$foundSplitPos) {
2172
            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
2173
            $encodedCharPos = strpos($lastChunk, '=');
2174
            if (false !== $encodedCharPos) {
2175
                // Found start of encoded character byte within $lookBack block.
2176
                // Check the encoded byte value (the 2 chars after the '=')
2177
                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
2178
                $dec = hexdec($hex);
2179
                if ($dec < 128) {
2180
                    // Single byte character.
2181
                    // If the encoded char was found at pos 0, it will fit
2182
                    // otherwise reduce maxLength to start of the encoded char
2183
                    if ($encodedCharPos > 0) {
2184
                        $maxLength -= $lookBack - $encodedCharPos;
2185
                    }
2186
                    $foundSplitPos = true;
2187
                } elseif ($dec >= 192) {
2188
                    // First byte of a multi byte character
2189
                    // Reduce maxLength to split at start of character
2190
                    $maxLength -= $lookBack - $encodedCharPos;
2191
                    $foundSplitPos = true;
2192
                } elseif ($dec < 192) {
2193
                    // Middle byte of a multi byte character, look further back
2194
                    $lookBack += 3;
2195
                }
2196
            } else {
2197
                // No encoded character found
2198
                $foundSplitPos = true;
2199
            }
2200
        }
2201
2202
        return $maxLength;
2203
    }
2204
2205
    /**
2206
     * Apply word wrapping to the message body.
2207
     * Wraps the message body to the number of chars set in the WordWrap property.
2208
     * You should only do this to plain-text bodies as wrapping HTML tags may break them.
2209
     * This is called automatically by createBody(), so you don't need to call it yourself.
2210
     */
2211
    public function setWordWrap()
2212
    {
2213
        if ($this->WordWrap < 1) {
2214
            return;
2215
        }
2216
2217
        switch ($this->message_type) {
2218
            case 'alt':
2219
            case 'alt_inline':
2220
            case 'alt_attach':
2221
            case 'alt_inline_attach':
2222
                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
2223
                break;
2224
            default:
2225
                $this->Body = $this->wrapText($this->Body, $this->WordWrap);
2226
                break;
2227
        }
2228
    }
2229
2230
    /**
2231
     * Assemble message headers.
2232
     *
2233
     * @return string The assembled headers
2234
     */
2235
    public function createHeader()
2236
    {
2237
        $result = '';
2238
2239
        $result .= $this->headerLine('Date', '' == $this->MessageDate ? self::rfcDate() : $this->MessageDate);
2240
2241
        // To be created automatically by mail()
2242
        if ($this->SingleTo) {
2243
            if ('mail' != $this->Mailer) {
2244
                foreach ($this->to as $toaddr) {
2245
                    $this->SingleToArray[] = $this->addrFormat($toaddr);
2246
                }
2247
            }
2248
        } else {
2249
            if (count($this->to) > 0) {
2250
                if ('mail' != $this->Mailer) {
2251
                    $result .= $this->addrAppend('To', $this->to);
2252
                }
2253
            } elseif (count($this->cc) == 0) {
2254
                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
2255
            }
2256
        }
2257
2258
        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
2259
2260
        // sendmail and mail() extract Cc from the header before sending
2261
        if (count($this->cc) > 0) {
2262
            $result .= $this->addrAppend('Cc', $this->cc);
2263
        }
2264
2265
        // sendmail and mail() extract Bcc from the header before sending
2266
        if ((
2267
                'sendmail' == $this->Mailer or 'qmail' == $this->Mailer or 'mail' == $this->Mailer
2268
            )
2269
            and count($this->bcc) > 0
2270
        ) {
2271
            $result .= $this->addrAppend('Bcc', $this->bcc);
2272
        }
2273
2274
        if (count($this->ReplyTo) > 0) {
2275
            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
2276
        }
2277
2278
        // mail() sets the subject itself
2279
        if ('mail' != $this->Mailer) {
2280
            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
2281
        }
2282
2283
        // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
2284
        // https://tools.ietf.org/html/rfc5322#section-3.6.4
2285
        if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) {
2286
            $this->lastMessageID = $this->MessageID;
2287
        } else {
2288
            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
2289
        }
2290
        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
2291
        if (null !== $this->Priority) {
2292
            $result .= $this->headerLine('X-Priority', $this->Priority);
2293
        }
2294
        if ('' == $this->XMailer) {
2295
            $result .= $this->headerLine(
2296
                'X-Mailer',
2297
                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
2298
            );
2299
        } else {
2300
            $myXmailer = trim($this->XMailer);
2301
            if ($myXmailer) {
2302
                $result .= $this->headerLine('X-Mailer', $myXmailer);
2303
            }
2304
        }
2305
2306
        if ('' != $this->ConfirmReadingTo) {
2307
            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
2308
        }
2309
2310
        // Add custom headers
2311
        foreach ($this->CustomHeader as $header) {
2312
            $result .= $this->headerLine(
2313
                trim($header[0]),
2314
                $this->encodeHeader(trim($header[1]))
2315
            );
2316
        }
2317
        if (!$this->sign_key_file) {
2318
            $result .= $this->headerLine('MIME-Version', '1.0');
2319
            $result .= $this->getMailMIME();
2320
        }
2321
2322
        return $result;
2323
    }
2324
2325
    /**
2326
     * Get the message MIME type headers.
2327
     *
2328
     * @return string
2329
     */
2330
    public function getMailMIME()
2331
    {
2332
        $result = '';
2333
        $ismultipart = true;
2334
        switch ($this->message_type) {
2335 View Code Duplication
            case 'inline':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2336
                $result .= $this->headerLine('Content-Type', 'multipart/related;');
2337
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2338
                break;
2339
            case 'attach':
2340
            case 'inline_attach':
2341
            case 'alt_attach':
2342 View Code Duplication
            case 'alt_inline_attach':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2343
                $result .= $this->headerLine('Content-Type', 'multipart/mixed;');
2344
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2345
                break;
2346
            case 'alt':
2347 View Code Duplication
            case 'alt_inline':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2348
                $result .= $this->headerLine('Content-Type', 'multipart/alternative;');
2349
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2350
                break;
2351
            default:
2352
                // Catches case 'plain': and case '':
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2353
                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
2354
                $ismultipart = false;
2355
                break;
2356
        }
2357
        // RFC1341 part 5 says 7bit is assumed if not specified
2358
        if ('7bit' != $this->Encoding) {
2359
            // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
2360
            if ($ismultipart) {
2361
                if ('8bit' == $this->Encoding) {
2362
                    $result .= $this->headerLine('Content-Transfer-Encoding', '8bit');
2363
                }
2364
                // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
2365
            } else {
2366
                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
2367
            }
2368
        }
2369
2370
        if ('mail' != $this->Mailer) {
2371
            $result .= static::$LE;
2372
        }
2373
2374
        return $result;
2375
    }
2376
2377
    /**
2378
     * Returns the whole MIME message.
2379
     * Includes complete headers and body.
2380
     * Only valid post preSend().
2381
     *
2382
     * @see PHPMailer::preSend()
2383
     *
2384
     * @return string
2385
     */
2386
    public function getSentMIMEMessage()
2387
    {
2388
        return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . static::$LE . static::$LE . $this->MIMEBody;
2389
    }
2390
2391
    /**
2392
     * Create a unique ID to use for boundaries.
2393
     *
2394
     * @return string
2395
     */
2396
    protected function generateId()
2397
    {
2398
        $len = 32; //32 bytes = 256 bits
2399
        if (function_exists('random_bytes')) {
2400
            $bytes = random_bytes($len);
2401
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
2402
            $bytes = openssl_random_pseudo_bytes($len);
2403
        } else {
2404
            //Use a hash to force the length to the same as the other methods
2405
            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
2406
        }
2407
2408
        //We don't care about messing up base64 format here, just want a random string
2409
        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
2410
    }
2411
2412
    /**
2413
     * Assemble the message body.
2414
     * Returns an empty string on failure.
2415
     *
2416
     * @throws Exception
2417
     *
2418
     * @return string The assembled message body
2419
     */
2420
    public function createBody()
2421
    {
2422
        $body = '';
2423
        //Create unique IDs and preset boundaries
2424
        $this->uniqueid = $this->generateId();
2425
        $this->boundary[1] = 'b1_' . $this->uniqueid;
2426
        $this->boundary[2] = 'b2_' . $this->uniqueid;
2427
        $this->boundary[3] = 'b3_' . $this->uniqueid;
2428
2429
        if ($this->sign_key_file) {
2430
            $body .= $this->getMailMIME() . static::$LE;
2431
        }
2432
2433
        $this->setWordWrap();
2434
2435
        $bodyEncoding = $this->Encoding;
2436
        $bodyCharSet = $this->CharSet;
2437
        //Can we do a 7-bit downgrade?
2438
        if ('8bit' == $bodyEncoding and !$this->has8bitChars($this->Body)) {
2439
            $bodyEncoding = '7bit';
2440
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2441
            $bodyCharSet = 'us-ascii';
2442
        }
2443
        //If lines are too long, and we're not already using an encoding that will shorten them,
2444
        //change to quoted-printable transfer encoding for the body part only
2445
        if ('base64' != $this->Encoding and static::hasLineLongerThanMax($this->Body)) {
2446
            $bodyEncoding = 'quoted-printable';
2447
        }
2448
2449
        $altBodyEncoding = $this->Encoding;
2450
        $altBodyCharSet = $this->CharSet;
2451
        //Can we do a 7-bit downgrade?
2452
        if ('8bit' == $altBodyEncoding and !$this->has8bitChars($this->AltBody)) {
2453
            $altBodyEncoding = '7bit';
2454
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2455
            $altBodyCharSet = 'us-ascii';
2456
        }
2457
        //If lines are too long, and we're not already using an encoding that will shorten them,
2458
        //change to quoted-printable transfer encoding for the alt body part only
2459
        if ('base64' != $altBodyEncoding and static::hasLineLongerThanMax($this->AltBody)) {
2460
            $altBodyEncoding = 'quoted-printable';
2461
        }
2462
        //Use this as a preamble in all multipart message types
2463
        $mimepre = 'This is a multi-part message in MIME format.' . static::$LE;
2464
        switch ($this->message_type) {
2465 View Code Duplication
            case 'inline':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2466
                $body .= $mimepre;
2467
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2468
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2469
                $body .= static::$LE;
2470
                $body .= $this->attachAll('inline', $this->boundary[1]);
2471
                break;
2472 View Code Duplication
            case 'attach':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2473
                $body .= $mimepre;
2474
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2475
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2476
                $body .= static::$LE;
2477
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2478
                break;
2479
            case 'inline_attach':
2480
                $body .= $mimepre;
2481
                $body .= $this->textLine('--' . $this->boundary[1]);
2482
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2483
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2484
                $body .= static::$LE;
2485
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
2486
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2487
                $body .= static::$LE;
2488
                $body .= $this->attachAll('inline', $this->boundary[2]);
2489
                $body .= static::$LE;
2490
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2491
                break;
2492
            case 'alt':
2493
                $body .= $mimepre;
2494
                $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2495
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2496
                $body .= static::$LE;
2497
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, 'text/html', $bodyEncoding);
2498
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2499
                $body .= static::$LE;
2500 View Code Duplication
                if (!empty($this->Ical)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2501
                    $body .= $this->getBoundary($this->boundary[1], '', 'text/calendar; method=REQUEST', '');
2502
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2503
                    $body .= static::$LE;
2504
                }
2505
                $body .= $this->endBoundary($this->boundary[1]);
2506
                break;
2507
            case 'alt_inline':
2508
                $body .= $mimepre;
2509
                $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2510
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2511
                $body .= static::$LE;
2512
                $body .= $this->textLine('--' . $this->boundary[1]);
2513
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2514
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2515
                $body .= static::$LE;
2516
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding);
2517
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2518
                $body .= static::$LE;
2519
                $body .= $this->attachAll('inline', $this->boundary[2]);
2520
                $body .= static::$LE;
2521
                $body .= $this->endBoundary($this->boundary[1]);
2522
                break;
2523
            case 'alt_attach':
2524
                $body .= $mimepre;
2525
                $body .= $this->textLine('--' . $this->boundary[1]);
2526
                $body .= $this->headerLine('Content-Type', 'multipart/alternative;');
2527
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2528
                $body .= static::$LE;
2529
                $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2530
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2531
                $body .= static::$LE;
2532
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding);
2533
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2534
                $body .= static::$LE;
2535 View Code Duplication
                if (!empty($this->Ical)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
2536
                    $body .= $this->getBoundary($this->boundary[2], '', 'text/calendar; method=REQUEST', '');
2537
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2538
                }
2539
                $body .= $this->endBoundary($this->boundary[2]);
2540
                $body .= static::$LE;
2541
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2542
                break;
2543
            case 'alt_inline_attach':
2544
                $body .= $mimepre;
2545
                $body .= $this->textLine('--' . $this->boundary[1]);
2546
                $body .= $this->headerLine('Content-Type', 'multipart/alternative;');
2547
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2548
                $body .= static::$LE;
2549
                $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2550
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2551
                $body .= static::$LE;
2552
                $body .= $this->textLine('--' . $this->boundary[2]);
2553
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2554
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"');
2555
                $body .= static::$LE;
2556
                $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, 'text/html', $bodyEncoding);
2557
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2558
                $body .= static::$LE;
2559
                $body .= $this->attachAll('inline', $this->boundary[3]);
2560
                $body .= static::$LE;
2561
                $body .= $this->endBoundary($this->boundary[2]);
2562
                $body .= static::$LE;
2563
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2564
                break;
2565
            default:
2566
                // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
2567
                //Reset the `Encoding` property in case we changed it for line length reasons
2568
                $this->Encoding = $bodyEncoding;
2569
                $body .= $this->encodeString($this->Body, $this->Encoding);
2570
                break;
2571
        }
2572
2573
        if ($this->isError()) {
2574
            $body = '';
2575
            if ($this->exceptions) {
2576
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
2577
            }
2578
        } elseif ($this->sign_key_file) {
2579
            try {
2580
                if (!defined('PKCS7_TEXT')) {
2581
                    throw new Exception($this->lang('extension_missing') . 'openssl');
2582
                }
2583
                // @TODO would be nice to use php://temp streams here
2584
                $file = tempnam(sys_get_temp_dir(), 'mail');
2585
                if (false === file_put_contents($file, $body)) {
2586
                    throw new Exception($this->lang('signing') . ' Could not write temp file');
2587
                }
2588
                $signed = tempnam(sys_get_temp_dir(), 'signed');
2589
                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
2590
                if (empty($this->sign_extracerts_file)) {
2591
                    $sign = @openssl_pkcs7_sign(
2592
                        $file,
2593
                        $signed,
2594
                        'file://' . realpath($this->sign_cert_file),
2595
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2596
                        []
2597
                    );
2598
                } else {
2599
                    $sign = @openssl_pkcs7_sign(
2600
                        $file,
2601
                        $signed,
2602
                        'file://' . realpath($this->sign_cert_file),
2603
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2604
                        [],
2605
                        PKCS7_DETACHED,
2606
                        $this->sign_extracerts_file
2607
                    );
2608
                }
2609
                @unlink($file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2610
                if ($sign) {
2611
                    $body = file_get_contents($signed);
2612
                    @unlink($signed);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2613
                    //The message returned by openssl contains both headers and body, so need to split them up
2614
                    $parts = explode("\n\n", $body, 2);
2615
                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
2616
                    $body = $parts[1];
2617
                } else {
2618
                    @unlink($signed);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2619
                    throw new Exception($this->lang('signing') . openssl_error_string());
2620
                }
2621
            } catch (Exception $exc) {
2622
                $body = '';
2623
                if ($this->exceptions) {
2624
                    throw $exc;
2625
                }
2626
            }
2627
        }
2628
2629
        return $body;
2630
    }
2631
2632
    /**
2633
     * Return the start of a message boundary.
2634
     *
2635
     * @param string $boundary
2636
     * @param string $charSet
2637
     * @param string $contentType
2638
     * @param string $encoding
2639
     *
2640
     * @return string
2641
     */
2642
    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
2643
    {
2644
        $result = '';
2645
        if ('' == $charSet) {
2646
            $charSet = $this->CharSet;
2647
        }
2648
        if ('' == $contentType) {
2649
            $contentType = $this->ContentType;
2650
        }
2651
        if ('' == $encoding) {
2652
            $encoding = $this->Encoding;
2653
        }
2654
        $result .= $this->textLine('--' . $boundary);
2655
        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
2656
        $result .= static::$LE;
2657
        // RFC1341 part 5 says 7bit is assumed if not specified
2658
        if ('7bit' != $encoding) {
2659
            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
2660
        }
2661
        $result .= static::$LE;
2662
2663
        return $result;
2664
    }
2665
2666
    /**
2667
     * Return the end of a message boundary.
2668
     *
2669
     * @param string $boundary
2670
     *
2671
     * @return string
2672
     */
2673
    protected function endBoundary($boundary)
2674
    {
2675
        return static::$LE . '--' . $boundary . '--' . static::$LE;
2676
    }
2677
2678
    /**
2679
     * Set the message type.
2680
     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
2681
     */
2682
    protected function setMessageType()
2683
    {
2684
        $type = [];
2685
        if ($this->alternativeExists()) {
2686
            $type[] = 'alt';
2687
        }
2688
        if ($this->inlineImageExists()) {
2689
            $type[] = 'inline';
2690
        }
2691
        if ($this->attachmentExists()) {
2692
            $type[] = 'attach';
2693
        }
2694
        $this->message_type = implode('_', $type);
2695
        if ('' == $this->message_type) {
2696
            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
2697
            $this->message_type = 'plain';
2698
        }
2699
    }
2700
2701
    /**
2702
     * Format a header line.
2703
     *
2704
     * @param string     $name
2705
     * @param string|int $value
2706
     *
2707
     * @return string
2708
     */
2709
    public function headerLine($name, $value)
2710
    {
2711
        return $name . ': ' . $value . static::$LE;
2712
    }
2713
2714
    /**
2715
     * Return a formatted mail line.
2716
     *
2717
     * @param string $value
2718
     *
2719
     * @return string
2720
     */
2721
    public function textLine($value)
2722
    {
2723
        return $value . static::$LE;
2724
    }
2725
2726
    /**
2727
     * Add an attachment from a path on the filesystem.
2728
     * Never use a user-supplied path to a file!
2729
     * Returns false if the file could not be found or read.
2730
     *
2731
     * @param string $path        Path to the attachment
2732
     * @param string $name        Overrides the attachment name
2733
     * @param string $encoding    File encoding (see $Encoding)
2734
     * @param string $type        File extension (MIME) type
2735
     * @param string $disposition Disposition to use
2736
     *
2737
     * @throws Exception
2738
     *
2739
     * @return bool
2740
     */
2741
    public function addAttachment($path, $name = '', $encoding = 'base64', $type = '', $disposition = 'attachment')
2742
    {
2743
        try {
2744
            if (!@is_file($path)) {
2745
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
2746
            }
2747
2748
            // If a MIME type is not specified, try to work it out from the file name
2749
            if ('' == $type) {
2750
                $type = static::filenameToType($path);
2751
            }
2752
2753
            $filename = basename($path);
2754
            if ('' == $name) {
2755
                $name = $filename;
2756
            }
2757
2758
            $this->attachment[] = [
2759
                0 => $path,
2760
                1 => $filename,
2761
                2 => $name,
2762
                3 => $encoding,
2763
                4 => $type,
2764
                5 => false, // isStringAttachment
2765
                6 => $disposition,
2766
                7 => $name,
2767
            ];
2768
        } catch (Exception $exc) {
2769
            $this->setError($exc->getMessage());
2770
            $this->edebug($exc->getMessage());
2771
            if ($this->exceptions) {
2772
                throw $exc;
2773
            }
2774
2775
            return false;
2776
        }
2777
2778
        return true;
2779
    }
2780
2781
    /**
2782
     * Return the array of attachments.
2783
     *
2784
     * @return array
2785
     */
2786
    public function getAttachments()
2787
    {
2788
        return $this->attachment;
2789
    }
2790
2791
    /**
2792
     * Attach all file, string, and binary attachments to the message.
2793
     * Returns an empty string on failure.
2794
     *
2795
     * @param string $disposition_type
2796
     * @param string $boundary
2797
     *
2798
     * @return string
2799
     */
2800
    protected function attachAll($disposition_type, $boundary)
2801
    {
2802
        // Return text of body
2803
        $mime = [];
2804
        $cidUniq = [];
2805
        $incl = [];
2806
2807
        // Add all attachments
2808
        foreach ($this->attachment as $attachment) {
2809
            // Check if it is a valid disposition_filter
2810
            if ($attachment[6] == $disposition_type) {
2811
                // Check for string attachment
2812
                $string = '';
2813
                $path = '';
2814
                $bString = $attachment[5];
2815
                if ($bString) {
2816
                    $string = $attachment[0];
2817
                } else {
2818
                    $path = $attachment[0];
2819
                }
2820
2821
                $inclhash = hash('sha256', serialize($attachment));
2822
                if (in_array($inclhash, $incl)) {
2823
                    continue;
2824
                }
2825
                $incl[] = $inclhash;
2826
                $name = $attachment[2];
2827
                $encoding = $attachment[3];
2828
                $type = $attachment[4];
2829
                $disposition = $attachment[6];
2830
                $cid = $attachment[7];
2831
                if ('inline' == $disposition and array_key_exists($cid, $cidUniq)) {
2832
                    continue;
2833
                }
2834
                $cidUniq[$cid] = true;
2835
2836
                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
2837
                //Only include a filename property if we have one
2838
                if (!empty($name)) {
2839
                    $mime[] = sprintf(
2840
                        'Content-Type: %s; name="%s"%s',
2841
                        $type,
2842
                        $this->encodeHeader($this->secureHeader($name)),
2843
                        static::$LE
2844
                    );
2845
                } else {
2846
                    $mime[] = sprintf(
2847
                        'Content-Type: %s%s',
2848
                        $type,
2849
                        static::$LE
2850
                    );
2851
                }
2852
                // RFC1341 part 5 says 7bit is assumed if not specified
2853
                if ('7bit' != $encoding) {
2854
                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
2855
                }
2856
2857
                if (!empty($cid)) {
2858
                    $mime[] = sprintf('Content-ID: <%s>%s', $cid, static::$LE);
2859
                }
2860
2861
                // If a filename contains any of these chars, it should be quoted,
2862
                // but not otherwise: RFC2183 & RFC2045 5.1
2863
                // Fixes a warning in IETF's msglint MIME checker
2864
                // Allow for bypassing the Content-Disposition header totally
2865
                if (!(empty($disposition))) {
2866
                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
2867
                    if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) {
2868
                        $mime[] = sprintf(
2869
                            'Content-Disposition: %s; filename="%s"%s',
2870
                            $disposition,
2871
                            $encoded_name,
2872
                            static::$LE . static::$LE
2873
                        );
2874
                    } else {
2875
                        if (!empty($encoded_name)) {
2876
                            $mime[] = sprintf(
2877
                                'Content-Disposition: %s; filename=%s%s',
2878
                                $disposition,
2879
                                $encoded_name,
2880
                                static::$LE . static::$LE
2881
                            );
2882
                        } else {
2883
                            $mime[] = sprintf(
2884
                                'Content-Disposition: %s%s',
2885
                                $disposition,
2886
                                static::$LE . static::$LE
2887
                            );
2888
                        }
2889
                    }
2890
                } else {
2891
                    $mime[] = static::$LE;
2892
                }
2893
2894
                // Encode as string attachment
2895
                if ($bString) {
2896
                    $mime[] = $this->encodeString($string, $encoding);
2897
                } else {
2898
                    $mime[] = $this->encodeFile($path, $encoding);
2899
                }
2900
                if ($this->isError()) {
2901
                    return '';
2902
                }
2903
                $mime[] = static::$LE;
2904
            }
2905
        }
2906
2907
        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
2908
2909
        return implode('', $mime);
2910
    }
2911
2912
    /**
2913
     * Encode a file attachment in requested format.
2914
     * Returns an empty string on failure.
2915
     *
2916
     * @param string $path     The full path to the file
2917
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
2918
     *
2919
     * @throws Exception
2920
     *
2921
     * @return string
2922
     */
2923
    protected function encodeFile($path, $encoding = 'base64')
2924
    {
2925
        try {
2926
            if (!file_exists($path)) {
2927
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
2928
            }
2929
            $file_buffer = file_get_contents($path);
2930
            if (false === $file_buffer) {
2931
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
2932
            }
2933
            $file_buffer = $this->encodeString($file_buffer, $encoding);
2934
2935
            return $file_buffer;
2936
        } catch (Exception $exc) {
2937
            $this->setError($exc->getMessage());
2938
2939
            return '';
2940
        }
2941
    }
2942
2943
    /**
2944
     * Encode a string in requested format.
2945
     * Returns an empty string on failure.
2946
     *
2947
     * @param string $str      The text to encode
2948
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable
2949
     *
2950
     * @return string
2951
     */
2952
    public function encodeString($str, $encoding = 'base64')
2953
    {
2954
        $encoded = '';
2955
        switch (strtolower($encoding)) {
2956
            case 'base64':
2957
                $encoded = chunk_split(
2958
                    base64_encode($str),
2959
                    static::STD_LINE_LENGTH - strlen(static::$LE),
2960
                    static::$LE
2961
                );
2962
                break;
2963
            case '7bit':
2964
            case '8bit':
2965
                $encoded = static::normalizeBreaks($str);
2966
                // Make sure it ends with a line break
2967
                if (substr($encoded, -(strlen(static::$LE))) != static::$LE) {
2968
                    $encoded .= static::$LE;
2969
                }
2970
                break;
2971
            case 'binary':
2972
                $encoded = $str;
2973
                break;
2974
            case 'quoted-printable':
2975
                $encoded = $this->encodeQP($str);
2976
                break;
2977
            default:
2978
                $this->setError($this->lang('encoding') . $encoding);
2979
                break;
2980
        }
2981
2982
        return $encoded;
2983
    }
2984
2985
    /**
2986
     * Encode a header value (not including its label) optimally.
2987
     * Picks shortest of Q, B, or none. Result includes folding if needed.
2988
     * See RFC822 definitions for phrase, comment and text positions.
2989
     *
2990
     * @param string $str      The header value to encode
2991
     * @param string $position What context the string will be used in
2992
     *
2993
     * @return string
2994
     */
2995
    public function encodeHeader($str, $position = 'text')
2996
    {
2997
        $matchcount = 0;
2998
        switch (strtolower($position)) {
2999
            case 'phrase':
3000
                if (!preg_match('/[\200-\377]/', $str)) {
3001
                    // Can't use addslashes as we don't know the value of magic_quotes_sybase
3002
                    $encoded = addcslashes($str, "\0..\37\177\\\"");
3003
                    if (($str == $encoded) and !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
3004
                        return $encoded;
3005
                    }
3006
3007
                    return "\"$encoded\"";
3008
                }
3009
                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
3010
                break;
3011
            /* @noinspection PhpMissingBreakStatementInspection */
3012
            case 'comment':
3013
                $matchcount = preg_match_all('/[()"]/', $str, $matches);
3014
            //fallthrough
3015
            case 'text':
3016
            default:
3017
                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
3018
                break;
3019
        }
3020
3021
        //RFCs specify a maximum line length of 78 chars, however mail() will sometimes
3022
        //corrupt messages with headers longer than 65 chars. See #818
3023
        $lengthsub = 'mail' == $this->Mailer ? 13 : 0;
3024
        $maxlen = static::STD_LINE_LENGTH - $lengthsub;
3025
        // Try to select the encoding which should produce the shortest output
3026
        if ($matchcount > strlen($str) / 3) {
3027
            // More than a third of the content will need encoding, so B encoding will be most efficient
3028
            $encoding = 'B';
3029
            //This calculation is:
3030
            // max line length
3031
            // - shorten to avoid mail() corruption
3032
            // - Q/B encoding char overhead ("` =?<charset>?[QB]?<content>?=`")
3033
            // - charset name length
3034
            $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet);
3035
            if ($this->hasMultiBytes($str)) {
3036
                // Use a custom function which correctly encodes and wraps long
3037
                // multibyte strings without breaking lines within a character
3038
                $encoded = $this->base64EncodeWrapMB($str, "\n");
3039
            } else {
3040
                $encoded = base64_encode($str);
3041
                $maxlen -= $maxlen % 4;
3042
                $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
3043
            }
3044
            $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
3045
        } elseif ($matchcount > 0) {
3046
            //1 or more chars need encoding, use Q-encode
3047
            $encoding = 'Q';
3048
            //Recalc max line length for Q encoding - see comments on B encode
3049
            $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet);
3050
            $encoded = $this->encodeQ($str, $position);
3051
            $encoded = $this->wrapText($encoded, $maxlen, true);
3052
            $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
3053
            $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
3054
        } elseif (strlen($str) > $maxlen) {
3055
            //No chars need encoding, but line is too long, so fold it
3056
            $encoded = trim($this->wrapText($str, $maxlen, false));
3057
            if ($str == $encoded) {
3058
                //Wrapping nicely didn't work, wrap hard instead
3059
                $encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE));
3060
            }
3061
            $encoded = str_replace(static::$LE, "\n", trim($encoded));
3062
            $encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded);
3063
        } else {
3064
            //No reformatting needed
3065
            return $str;
3066
        }
3067
3068
        return trim(static::normalizeBreaks($encoded));
3069
    }
3070
3071
    /**
3072
     * Check if a string contains multi-byte characters.
3073
     *
3074
     * @param string $str multi-byte text to wrap encode
3075
     *
3076
     * @return bool
3077
     */
3078
    public function hasMultiBytes($str)
3079
    {
3080
        if (function_exists('mb_strlen')) {
3081
            return strlen($str) > mb_strlen($str, $this->CharSet);
3082
        }
3083
3084
        // Assume no multibytes (we can't handle without mbstring functions anyway)
3085
        return false;
3086
    }
3087
3088
    /**
3089
     * Does a string contain any 8-bit chars (in any charset)?
3090
     *
3091
     * @param string $text
3092
     *
3093
     * @return bool
3094
     */
3095
    public function has8bitChars($text)
3096
    {
3097
        return (bool) preg_match('/[\x80-\xFF]/', $text);
3098
    }
3099
3100
    /**
3101
     * Encode and wrap long multibyte strings for mail headers
3102
     * without breaking lines within a character.
3103
     * Adapted from a function by paravoid.
3104
     *
3105
     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
3106
     *
3107
     * @param string $str       multi-byte text to wrap encode
3108
     * @param string $linebreak string to use as linefeed/end-of-line
3109
     *
3110
     * @return string
3111
     */
3112
    public function base64EncodeWrapMB($str, $linebreak = null)
3113
    {
3114
        $start = '=?' . $this->CharSet . '?B?';
3115
        $end = '?=';
3116
        $encoded = '';
3117
        if (null === $linebreak) {
3118
            $linebreak = static::$LE;
3119
        }
3120
3121
        $mb_length = mb_strlen($str, $this->CharSet);
3122
        // Each line must have length <= 75, including $start and $end
3123
        $length = 75 - strlen($start) - strlen($end);
3124
        // Average multi-byte ratio
3125
        $ratio = $mb_length / strlen($str);
3126
        // Base64 has a 4:3 ratio
3127
        $avgLength = floor($length * $ratio * .75);
3128
3129
        for ($i = 0; $i < $mb_length; $i += $offset) {
3130
            $lookBack = 0;
3131
            do {
3132
                $offset = $avgLength - $lookBack;
3133
                $chunk = mb_substr($str, $i, $offset, $this->CharSet);
3134
                $chunk = base64_encode($chunk);
3135
                ++$lookBack;
3136
            } while (strlen($chunk) > $length);
3137
            $encoded .= $chunk . $linebreak;
3138
        }
3139
3140
        // Chomp the last linefeed
3141
        return substr($encoded, 0, -strlen($linebreak));
3142
    }
3143
3144
    /**
3145
     * Encode a string in quoted-printable format.
3146
     * According to RFC2045 section 6.7.
3147
     *
3148
     * @param string $string The text to encode
3149
     *
3150
     * @return string
3151
     */
3152
    public function encodeQP($string)
3153
    {
3154
        return static::normalizeBreaks(quoted_printable_encode($string));
3155
    }
3156
3157
    /**
3158
     * Encode a string using Q encoding.
3159
     *
3160
     * @see http://tools.ietf.org/html/rfc2047#section-4.2
3161
     *
3162
     * @param string $str      the text to encode
3163
     * @param string $position Where the text is going to be used, see the RFC for what that means
3164
     *
3165
     * @return string
3166
     */
3167
    public function encodeQ($str, $position = 'text')
3168
    {
3169
        // There should not be any EOL in the string
3170
        $pattern = '';
3171
        $encoded = str_replace(["\r", "\n"], '', $str);
3172
        switch (strtolower($position)) {
3173
            case 'phrase':
3174
                // RFC 2047 section 5.3
3175
                $pattern = '^A-Za-z0-9!*+\/ -';
3176
                break;
3177
            /*
3178
             * RFC 2047 section 5.2.
3179
             * Build $pattern without including delimiters and []
3180
             */
3181
            /* @noinspection PhpMissingBreakStatementInspection */
3182
            case 'comment':
3183
                $pattern = '\(\)"';
3184
            /* Intentional fall through */
3185
            case 'text':
3186
            default:
3187
                // RFC 2047 section 5.1
3188
                // Replace every high ascii, control, =, ? and _ characters
3189
                $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
3190
                break;
3191
        }
3192
        $matches = [];
3193
        if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
3194
            // If the string contains an '=', make sure it's the first thing we replace
3195
            // so as to avoid double-encoding
3196
            $eqkey = array_search('=', $matches[0]);
3197
            if (false !== $eqkey) {
3198
                unset($matches[0][$eqkey]);
3199
                array_unshift($matches[0], '=');
3200
            }
3201
            foreach (array_unique($matches[0]) as $char) {
3202
                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
3203
            }
3204
        }
3205
        // Replace spaces with _ (more readable than =20)
3206
        // RFC 2047 section 4.2(2)
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3207
        return str_replace(' ', '_', $encoded);
3208
    }
3209
3210
    /**
3211
     * Add a string or binary attachment (non-filesystem).
3212
     * This method can be used to attach ascii or binary data,
3213
     * such as a BLOB record from a database.
3214
     *
3215
     * @param string $string      String attachment data
3216
     * @param string $filename    Name of the attachment
3217
     * @param string $encoding    File encoding (see $Encoding)
3218
     * @param string $type        File extension (MIME) type
3219
     * @param string $disposition Disposition to use
3220
     */
3221
    public function addStringAttachment(
3222
        $string,
3223
        $filename,
3224
        $encoding = 'base64',
3225
        $type = '',
3226
        $disposition = 'attachment'
3227
    ) {
3228
        // If a MIME type is not specified, try to work it out from the file name
3229
        if ('' == $type) {
3230
            $type = static::filenameToType($filename);
3231
        }
3232
        // Append to $attachment array
3233
        $this->attachment[] = [
3234
            0 => $string,
3235
            1 => $filename,
3236
            2 => basename($filename),
3237
            3 => $encoding,
3238
            4 => $type,
3239
            5 => true, // isStringAttachment
3240
            6 => $disposition,
3241
            7 => 0,
3242
        ];
3243
    }
3244
3245
    /**
3246
     * Add an embedded (inline) attachment from a file.
3247
     * This can include images, sounds, and just about any other document type.
3248
     * These differ from 'regular' attachments in that they are intended to be
3249
     * displayed inline with the message, not just attached for download.
3250
     * This is used in HTML messages that embed the images
3251
     * the HTML refers to using the $cid value.
3252
     * Never use a user-supplied path to a file!
3253
     *
3254
     * @param string $path        Path to the attachment
3255
     * @param string $cid         Content ID of the attachment; Use this to reference
3256
     *                            the content when using an embedded image in HTML
3257
     * @param string $name        Overrides the attachment name
3258
     * @param string $encoding    File encoding (see $Encoding)
3259
     * @param string $type        File MIME type
3260
     * @param string $disposition Disposition to use
3261
     *
3262
     * @return bool True on successfully adding an attachment
3263
     */
3264
    public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = '', $disposition = 'inline')
3265
    {
3266
        if (!@is_file($path)) {
3267
            $this->setError($this->lang('file_access') . $path);
3268
3269
            return false;
3270
        }
3271
3272
        // If a MIME type is not specified, try to work it out from the file name
3273
        if ('' == $type) {
3274
            $type = static::filenameToType($path);
3275
        }
3276
3277
        $filename = basename($path);
3278
        if ('' == $name) {
3279
            $name = $filename;
3280
        }
3281
3282
        // Append to $attachment array
3283
        $this->attachment[] = [
3284
            0 => $path,
3285
            1 => $filename,
3286
            2 => $name,
3287
            3 => $encoding,
3288
            4 => $type,
3289
            5 => false, // isStringAttachment
3290
            6 => $disposition,
3291
            7 => $cid,
3292
        ];
3293
3294
        return true;
3295
    }
3296
3297
    /**
3298
     * Add an embedded stringified attachment.
3299
     * This can include images, sounds, and just about any other document type.
3300
     * Be sure to set the $type to an image type for images:
3301
     * JPEG images use 'image/jpeg', GIF uses 'image/gif', PNG uses 'image/png'.
3302
     *
3303
     * @param string $string      The attachment binary data
3304
     * @param string $cid         Content ID of the attachment; Use this to reference
3305
     *                            the content when using an embedded image in HTML
3306
     * @param string $name
3307
     * @param string $encoding    File encoding (see $Encoding)
3308
     * @param string $type        MIME type
3309
     * @param string $disposition Disposition to use
3310
     *
3311
     * @return bool True on successfully adding an attachment
3312
     */
3313
    public function addStringEmbeddedImage(
3314
        $string,
3315
        $cid,
3316
        $name = '',
3317
        $encoding = 'base64',
3318
        $type = '',
3319
        $disposition = 'inline'
3320
    ) {
3321
        // If a MIME type is not specified, try to work it out from the name
3322
        if ('' == $type and !empty($name)) {
3323
            $type = static::filenameToType($name);
3324
        }
3325
3326
        // Append to $attachment array
3327
        $this->attachment[] = [
3328
            0 => $string,
3329
            1 => $name,
3330
            2 => $name,
3331
            3 => $encoding,
3332
            4 => $type,
3333
            5 => true, // isStringAttachment
3334
            6 => $disposition,
3335
            7 => $cid,
3336
        ];
3337
3338
        return true;
3339
    }
3340
3341
    /**
3342
     * Check if an embedded attachment is present with this cid.
3343
     *
3344
     * @param string $cid
3345
     *
3346
     * @return bool
3347
     */
3348 View Code Duplication
    protected function cidExists($cid)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
3349
    {
3350
        foreach ($this->attachment as $attachment) {
3351
            if ('inline' == $attachment[6] and $cid == $attachment[7]) {
3352
                return true;
3353
            }
3354
        }
3355
3356
        return false;
3357
    }
3358
3359
    /**
3360
     * Check if an inline attachment is present.
3361
     *
3362
     * @return bool
3363
     */
3364 View Code Duplication
    public function inlineImageExists()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
3365
    {
3366
        foreach ($this->attachment as $attachment) {
3367
            if ($attachment[6] == 'inline') {
3368
                return true;
3369
            }
3370
        }
3371
3372
        return false;
3373
    }
3374
3375
    /**
3376
     * Check if an attachment (non-inline) is present.
3377
     *
3378
     * @return bool
3379
     */
3380
    public function attachmentExists()
3381
    {
3382
        foreach ($this->attachment as $attachment) {
3383
            if ($attachment[6] == 'attachment') {
3384
                return true;
3385
            }
3386
        }
3387
3388
        return false;
3389
    }
3390
3391
    /**
3392
     * Check if this message has an alternative body set.
3393
     *
3394
     * @return bool
3395
     */
3396
    public function alternativeExists()
3397
    {
3398
        return !empty($this->AltBody);
3399
    }
3400
3401
    /**
3402
     * Clear queued addresses of given kind.
3403
     *
3404
     * @param string $kind 'to', 'cc', or 'bcc'
3405
     */
3406
    public function clearQueuedAddresses($kind)
3407
    {
3408
        $this->RecipientsQueue = array_filter(
3409
            $this->RecipientsQueue,
3410
            function ($params) use ($kind) {
3411
                return $params[0] != $kind;
3412
            }
3413
        );
3414
    }
3415
3416
    /**
3417
     * Clear all To recipients.
3418
     */
3419
    public function clearAddresses()
3420
    {
3421
        foreach ($this->to as $to) {
3422
            unset($this->all_recipients[strtolower($to[0])]);
3423
        }
3424
        $this->to = [];
3425
        $this->clearQueuedAddresses('to');
3426
    }
3427
3428
    /**
3429
     * Clear all CC recipients.
3430
     */
3431 View Code Duplication
    public function clearCCs()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
3432
    {
3433
        foreach ($this->cc as $cc) {
3434
            unset($this->all_recipients[strtolower($cc[0])]);
3435
        }
3436
        $this->cc = [];
3437
        $this->clearQueuedAddresses('cc');
3438
    }
3439
3440
    /**
3441
     * Clear all BCC recipients.
3442
     */
3443 View Code Duplication
    public function clearBCCs()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
3444
    {
3445
        foreach ($this->bcc as $bcc) {
3446
            unset($this->all_recipients[strtolower($bcc[0])]);
3447
        }
3448
        $this->bcc = [];
3449
        $this->clearQueuedAddresses('bcc');
3450
    }
3451
3452
    /**
3453
     * Clear all ReplyTo recipients.
3454
     */
3455
    public function clearReplyTos()
3456
    {
3457
        $this->ReplyTo = [];
3458
        $this->ReplyToQueue = [];
3459
    }
3460
3461
    /**
3462
     * Clear all recipient types.
3463
     */
3464
    public function clearAllRecipients()
3465
    {
3466
        $this->to = [];
3467
        $this->cc = [];
3468
        $this->bcc = [];
3469
        $this->all_recipients = [];
3470
        $this->RecipientsQueue = [];
3471
    }
3472
3473
    /**
3474
     * Clear all filesystem, string, and binary attachments.
3475
     */
3476
    public function clearAttachments()
3477
    {
3478
        $this->attachment = [];
3479
    }
3480
3481
    /**
3482
     * Clear all custom headers.
3483
     */
3484
    public function clearCustomHeaders()
3485
    {
3486
        $this->CustomHeader = [];
3487
    }
3488
3489
    /**
3490
     * Add an error message to the error container.
3491
     *
3492
     * @param string $msg
3493
     */
3494
    protected function setError($msg)
3495
    {
3496
        ++$this->error_count;
3497
        if ('smtp' == $this->Mailer and null !== $this->smtp) {
3498
            $lasterror = $this->smtp->getError();
3499
            if (!empty($lasterror['error'])) {
3500
                $msg .= $this->lang('smtp_error') . $lasterror['error'];
3501
                if (!empty($lasterror['detail'])) {
3502
                    $msg .= ' Detail: ' . $lasterror['detail'];
3503
                }
3504
                if (!empty($lasterror['smtp_code'])) {
3505
                    $msg .= ' SMTP code: ' . $lasterror['smtp_code'];
3506
                }
3507
                if (!empty($lasterror['smtp_code_ex'])) {
3508
                    $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex'];
3509
                }
3510
            }
3511
        }
3512
        $this->ErrorInfo = $msg;
3513
    }
3514
3515
    /**
3516
     * Return an RFC 822 formatted date.
3517
     *
3518
     * @return string
3519
     */
3520
    public static function rfcDate()
3521
    {
3522
        // Set the time zone to whatever the default is to avoid 500 errors
3523
        // Will default to UTC if it's not set properly in php.ini
3524
        date_default_timezone_set(@date_default_timezone_get());
3525
3526
        return date('D, j M Y H:i:s O');
3527
    }
3528
3529
    /**
3530
     * Get the server hostname.
3531
     * Returns 'localhost.localdomain' if unknown.
3532
     *
3533
     * @return string
3534
     */
3535
    protected function serverHostname()
3536
    {
3537
        $result = '';
3538
        if (!empty($this->Hostname)) {
3539
            $result = $this->Hostname;
3540
        } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER)) {
3541
            $result = $_SERVER['SERVER_NAME'];
3542
        } elseif (function_exists('gethostname') and gethostname() !== false) {
3543
            $result = gethostname();
3544
        } elseif (php_uname('n') !== false) {
3545
            $result = php_uname('n');
3546
        }
3547
        if (!static::isValidHost($result)) {
3548
            return 'localhost.localdomain';
3549
        }
3550
3551
        return $result;
3552
    }
3553
3554
    /**
3555
     * Validate whether a string contains a valid value to use as a hostname or IP address.
3556
     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
3557
     *
3558
     * @param string $host The host name or IP address to check
3559
     *
3560
     * @return bool
3561
     */
3562
    public static function isValidHost($host)
3563
    {
3564
        //Simple syntax limits
3565
        if (empty($host)
3566
            or !is_string($host)
3567
            or strlen($host) > 256
3568
        ) {
3569
            return false;
3570
        }
3571
        //Looks like a bracketed IPv6 address
3572
        if (trim($host, '[]') != $host) {
3573
            return (bool) filter_var(trim($host, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
3574
        }
3575
        //If removing all the dots results in a numeric string, it must be an IPv4 address.
3576
        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
3577
        if (is_numeric(str_replace('.', '', $host))) {
3578
            //Is it a valid IPv4 address?
3579
            return (bool) filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
3580
        }
3581
        if (filter_var('http://' . $host, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)) {
3582
            //Is it a syntactically valid hostname?
3583
            return true;
3584
        }
3585
3586
        return false;
3587
    }
3588
3589
    /**
3590
     * Get an error message in the current language.
3591
     *
3592
     * @param string $key
3593
     *
3594
     * @return string
3595
     */
3596
    protected function lang($key)
3597
    {
3598
        if (count($this->language) < 1) {
3599
            $this->setLanguage('en'); // set the default language
3600
        }
3601
3602
        if (array_key_exists($key, $this->language)) {
3603
            if ('smtp_connect_failed' == $key) {
3604
                //Include a link to troubleshooting docs on SMTP connection failure
3605
                //this is by far the biggest cause of support questions
3606
                //but it's usually not PHPMailer's fault.
3607
                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
3608
            }
3609
3610
            return $this->language[$key];
3611
        }
3612
3613
        //Return the key as a fallback
3614
        return $key;
3615
    }
3616
3617
    /**
3618
     * Check if an error occurred.
3619
     *
3620
     * @return bool True if an error did occur
3621
     */
3622
    public function isError()
3623
    {
3624
        return $this->error_count > 0;
3625
    }
3626
3627
    /**
3628
     * Add a custom header.
3629
     * $name value can be overloaded to contain
3630
     * both header name and value (name:value).
3631
     *
3632
     * @param string      $name  Custom header name
3633
     * @param string|null $value Header value
3634
     */
3635
    public function addCustomHeader($name, $value = null)
3636
    {
3637
        if (null === $value) {
3638
            // Value passed in as name:value
3639
            $this->CustomHeader[] = explode(':', $name, 2);
3640
        } else {
3641
            $this->CustomHeader[] = [$name, $value];
3642
        }
3643
    }
3644
3645
    /**
3646
     * Returns all custom headers.
3647
     *
3648
     * @return array
3649
     */
3650
    public function getCustomHeaders()
3651
    {
3652
        return $this->CustomHeader;
3653
    }
3654
3655
    /**
3656
     * Create a message body from an HTML string.
3657
     * Automatically inlines images and creates a plain-text version by converting the HTML,
3658
     * overwriting any existing values in Body and AltBody.
3659
     * Do not source $message content from user input!
3660
     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
3661
     * will look for an image file in $basedir/images/a.png and convert it to inline.
3662
     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
3663
     * Converts data-uri images into embedded attachments.
3664
     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
3665
     *
3666
     * @param string        $message  HTML message string
3667
     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
3668
     * @param bool|callable $advanced Whether to use the internal HTML to text converter
3669
     *                                or your own custom converter @see PHPMailer::html2text()
3670
     *
3671
     * @return string $message The transformed message Body
3672
     */
3673
    public function msgHTML($message, $basedir = '', $advanced = false)
3674
    {
3675
        preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images);
3676
        if (array_key_exists(2, $images)) {
3677 View Code Duplication
            if (strlen($basedir) > 1 && substr($basedir, -1) != '/') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
3678
                // Ensure $basedir has a trailing /
3679
                $basedir .= '/';
3680
            }
3681
            foreach ($images[2] as $imgindex => $url) {
3682
                // Convert data URIs into embedded images
3683
                //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
3684
                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
3685
                    if (count($match) == 4 and 'base64' == $match[2]) {
3686
                        $data = base64_decode($match[3]);
3687
                    } elseif ('' == $match[2]) {
3688
                        $data = rawurldecode($match[3]);
3689
                    } else {
3690
                        //Not recognised so leave it alone
3691
                        continue;
3692
                    }
3693
                    //Hash the decoded data, not the URL so that the same data-URI image used in multiple places
3694
                    //will only be embedded once, even if it used a different encoding
3695
                    $cid = hash('sha256', $data) . '@phpmailer.0'; // RFC2392 S 2
3696
3697
                    if (!$this->cidExists($cid)) {
3698
                        $this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, 'base64', $match[1]);
3699
                    }
3700
                    $message = str_replace(
3701
                        $images[0][$imgindex],
3702
                        $images[1][$imgindex] . '="cid:' . $cid . '"',
3703
                        $message
3704
                    );
3705
                    continue;
3706
                }
3707
                if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
3708
                    !empty($basedir)
3709
                    // Ignore URLs containing parent dir traversal (..)
3710
                    and (strpos($url, '..') === false)
3711
                    // Do not change urls that are already inline images
3712
                    and substr($url, 0, 4) !== 'cid:'
3713
                    // Do not change absolute URLs, including anonymous protocol
3714
                    and !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
3715
                ) {
3716
                    $filename = basename($url);
3717
                    $directory = dirname($url);
3718
                    if ('.' == $directory) {
3719
                        $directory = '';
3720
                    }
3721
                    $cid = hash('sha256', $url) . '@phpmailer.0'; // RFC2392 S 2
3722 View Code Duplication
                    if (strlen($basedir) > 1 and substr($basedir, -1) != '/') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
3723
                        $basedir .= '/';
3724
                    }
3725
                    if (strlen($directory) > 1 and substr($directory, -1) != '/') {
3726
                        $directory .= '/';
3727
                    }
3728
                    if ($this->addEmbeddedImage(
3729
                        $basedir . $directory . $filename,
3730
                        $cid,
3731
                        $filename,
3732
                        'base64',
3733
                        static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
3734
                    )
3735
                    ) {
3736
                        $message = preg_replace(
3737
                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
3738
                            $images[1][$imgindex] . '="cid:' . $cid . '"',
3739
                            $message
3740
                        );
3741
                    }
3742
                }
3743
            }
3744
        }
3745
        $this->isHTML(true);
3746
        // Convert all message body line breaks to LE, makes quoted-printable encoding work much better
3747
        $this->Body = static::normalizeBreaks($message);
3748
        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
3749
        if (!$this->alternativeExists()) {
3750
            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
3751
                . static::$LE;
3752
        }
3753
3754
        return $this->Body;
3755
    }
3756
3757
    /**
3758
     * Convert an HTML string into plain text.
3759
     * This is used by msgHTML().
3760
     * Note - older versions of this function used a bundled advanced converter
3761
     * which was removed for license reasons in #232.
3762
     * Example usage:
3763
     *
3764
     * ```php
3765
     * // Use default conversion
3766
     * $plain = $mail->html2text($html);
3767
     * // Use your own custom converter
3768
     * $plain = $mail->html2text($html, function($html) {
3769
     *     $converter = new MyHtml2text($html);
3770
     *     return $converter->get_text();
3771
     * });
3772
     * ```
3773
     *
3774
     * @param string        $html     The HTML text to convert
3775
     * @param bool|callable $advanced Any boolean value to use the internal converter,
3776
     *                                or provide your own callable for custom conversion
3777
     *
3778
     * @return string
3779
     */
3780
    public function html2text($html, $advanced = false)
3781
    {
3782
        if (is_callable($advanced)) {
3783
            return call_user_func($advanced, $html);
3784
        }
3785
3786
        return html_entity_decode(
3787
            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
3788
            ENT_QUOTES,
3789
            $this->CharSet
3790
        );
3791
    }
3792
3793
    /**
3794
     * Get the MIME type for a file extension.
3795
     *
3796
     * @param string $ext File extension
3797
     *
3798
     * @return string MIME type of file
3799
     */
3800
    public static function _mime_types($ext = '')
3801
    {
3802
        $mimes = [
3803
            'xl' => 'application/excel',
3804
            'js' => 'application/javascript',
3805
            'hqx' => 'application/mac-binhex40',
3806
            'cpt' => 'application/mac-compactpro',
3807
            'bin' => 'application/macbinary',
3808
            'doc' => 'application/msword',
3809
            'word' => 'application/msword',
3810
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
3811
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
3812
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
3813
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
3814
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
3815
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
3816
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
3817
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
3818
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
3819
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
3820
            'class' => 'application/octet-stream',
3821
            'dll' => 'application/octet-stream',
3822
            'dms' => 'application/octet-stream',
3823
            'exe' => 'application/octet-stream',
3824
            'lha' => 'application/octet-stream',
3825
            'lzh' => 'application/octet-stream',
3826
            'psd' => 'application/octet-stream',
3827
            'sea' => 'application/octet-stream',
3828
            'so' => 'application/octet-stream',
3829
            'oda' => 'application/oda',
3830
            'pdf' => 'application/pdf',
3831
            'ai' => 'application/postscript',
3832
            'eps' => 'application/postscript',
3833
            'ps' => 'application/postscript',
3834
            'smi' => 'application/smil',
3835
            'smil' => 'application/smil',
3836
            'mif' => 'application/vnd.mif',
3837
            'xls' => 'application/vnd.ms-excel',
3838
            'ppt' => 'application/vnd.ms-powerpoint',
3839
            'wbxml' => 'application/vnd.wap.wbxml',
3840
            'wmlc' => 'application/vnd.wap.wmlc',
3841
            'dcr' => 'application/x-director',
3842
            'dir' => 'application/x-director',
3843
            'dxr' => 'application/x-director',
3844
            'dvi' => 'application/x-dvi',
3845
            'gtar' => 'application/x-gtar',
3846
            'php3' => 'application/x-httpd-php',
3847
            'php4' => 'application/x-httpd-php',
3848
            'php' => 'application/x-httpd-php',
3849
            'phtml' => 'application/x-httpd-php',
3850
            'phps' => 'application/x-httpd-php-source',
3851
            'swf' => 'application/x-shockwave-flash',
3852
            'sit' => 'application/x-stuffit',
3853
            'tar' => 'application/x-tar',
3854
            'tgz' => 'application/x-tar',
3855
            'xht' => 'application/xhtml+xml',
3856
            'xhtml' => 'application/xhtml+xml',
3857
            'zip' => 'application/zip',
3858
            'mid' => 'audio/midi',
3859
            'midi' => 'audio/midi',
3860
            'mp2' => 'audio/mpeg',
3861
            'mp3' => 'audio/mpeg',
3862
            'mpga' => 'audio/mpeg',
3863
            'aif' => 'audio/x-aiff',
3864
            'aifc' => 'audio/x-aiff',
3865
            'aiff' => 'audio/x-aiff',
3866
            'ram' => 'audio/x-pn-realaudio',
3867
            'rm' => 'audio/x-pn-realaudio',
3868
            'rpm' => 'audio/x-pn-realaudio-plugin',
3869
            'ra' => 'audio/x-realaudio',
3870
            'wav' => 'audio/x-wav',
3871
            'bmp' => 'image/bmp',
3872
            'gif' => 'image/gif',
3873
            'jpeg' => 'image/jpeg',
3874
            'jpe' => 'image/jpeg',
3875
            'jpg' => 'image/jpeg',
3876
            'png' => 'image/png',
3877
            'tiff' => 'image/tiff',
3878
            'tif' => 'image/tiff',
3879
            'eml' => 'message/rfc822',
3880
            'css' => 'text/css',
3881
            'html' => 'text/html',
3882
            'htm' => 'text/html',
3883
            'shtml' => 'text/html',
3884
            'log' => 'text/plain',
3885
            'text' => 'text/plain',
3886
            'txt' => 'text/plain',
3887
            'rtx' => 'text/richtext',
3888
            'rtf' => 'text/rtf',
3889
            'vcf' => 'text/vcard',
3890
            'vcard' => 'text/vcard',
3891
            'ics' => 'text/calendar',
3892
            'xml' => 'text/xml',
3893
            'xsl' => 'text/xml',
3894
            'mpeg' => 'video/mpeg',
3895
            'mpe' => 'video/mpeg',
3896
            'mpg' => 'video/mpeg',
3897
            'mov' => 'video/quicktime',
3898
            'qt' => 'video/quicktime',
3899
            'rv' => 'video/vnd.rn-realvideo',
3900
            'avi' => 'video/x-msvideo',
3901
            'movie' => 'video/x-sgi-movie',
3902
        ];
3903
        if (array_key_exists(strtolower($ext), $mimes)) {
3904
            return $mimes[strtolower($ext)];
3905
        }
3906
3907
        return 'application/octet-stream';
3908
    }
3909
3910
    /**
3911
     * Map a file name to a MIME type.
3912
     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
3913
     *
3914
     * @param string $filename A file name or full path, does not need to exist as a file
3915
     *
3916
     * @return string
3917
     */
3918
    public static function filenameToType($filename)
3919
    {
3920
        // In case the path is a URL, strip any query string before getting extension
3921
        $qpos = strpos($filename, '?');
3922
        if (false !== $qpos) {
3923
            $filename = substr($filename, 0, $qpos);
3924
        }
3925
        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
3926
3927
        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 3925 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...
3928
    }
3929
3930
    /**
3931
     * Multi-byte-safe pathinfo replacement.
3932
     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
3933
     *
3934
     * @see    http://www.php.net/manual/en/function.pathinfo.php#107461
3935
     *
3936
     * @param string     $path    A filename or path, does not need to exist as a file
3937
     * @param int|string $options Either a PATHINFO_* constant,
3938
     *                            or a string name to return only the specified piece
3939
     *
3940
     * @return string|array
3941
     */
3942
    public static function mb_pathinfo($path, $options = null)
3943
    {
3944
        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
3945
        $pathinfo = [];
3946
        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$#im', $path, $pathinfo)) {
3947
            if (array_key_exists(1, $pathinfo)) {
3948
                $ret['dirname'] = $pathinfo[1];
3949
            }
3950
            if (array_key_exists(2, $pathinfo)) {
3951
                $ret['basename'] = $pathinfo[2];
3952
            }
3953
            if (array_key_exists(5, $pathinfo)) {
3954
                $ret['extension'] = $pathinfo[5];
3955
            }
3956
            if (array_key_exists(3, $pathinfo)) {
3957
                $ret['filename'] = $pathinfo[3];
3958
            }
3959
        }
3960
        switch ($options) {
3961
            case PATHINFO_DIRNAME:
3962
            case 'dirname':
3963
                return $ret['dirname'];
3964
            case PATHINFO_BASENAME:
3965
            case 'basename':
3966
                return $ret['basename'];
3967
            case PATHINFO_EXTENSION:
3968
            case 'extension':
3969
                return $ret['extension'];
3970
            case PATHINFO_FILENAME:
3971
            case 'filename':
3972
                return $ret['filename'];
3973
            default:
3974
                return $ret;
3975
        }
3976
    }
3977
3978
    /**
3979
     * Set or reset instance properties.
3980
     * You should avoid this function - it's more verbose, less efficient, more error-prone and
3981
     * harder to debug than setting properties directly.
3982
     * Usage Example:
3983
     * `$mail->set('SMTPSecure', 'tls');`
3984
     *   is the same as:
3985
     * `$mail->SMTPSecure = 'tls';`.
3986
     *
3987
     * @param string $name  The property name to set
3988
     * @param mixed  $value The value to set the property to
3989
     *
3990
     * @return bool
3991
     */
3992
    public function set($name, $value = '')
3993
    {
3994
        if (property_exists($this, $name)) {
3995
            $this->$name = $value;
3996
3997
            return true;
3998
        }
3999
        $this->setError($this->lang('variable_set') . $name);
4000
4001
        return false;
4002
    }
4003
4004
    /**
4005
     * Strip newlines to prevent header injection.
4006
     *
4007
     * @param string $str
4008
     *
4009
     * @return string
4010
     */
4011
    public function secureHeader($str)
4012
    {
4013
        return trim(str_replace(["\r", "\n"], '', $str));
4014
    }
4015
4016
    /**
4017
     * Normalize line breaks in a string.
4018
     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
4019
     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
4020
     *
4021
     * @param string $text
4022
     * @param string $breaktype What kind of line break to use; defaults to static::$LE
4023
     *
4024
     * @return string
4025
     */
4026
    public static function normalizeBreaks($text, $breaktype = null)
4027
    {
4028
        if (null === $breaktype) {
4029
            $breaktype = static::$LE;
4030
        }
4031
        // Normalise to \n
4032
        $text = str_replace(["\r\n", "\r"], "\n", $text);
4033
        // Now convert LE as needed
4034
        if ("\n" !== static::$LE) {
4035
            $text = str_replace("\n", $breaktype, $text);
4036
        }
4037
4038
        return $text;
4039
    }
4040
4041
    /**
4042
     * Return the current line break format string.
4043
     *
4044
     * @return string
4045
     */
4046
    public static function getLE()
4047
    {
4048
        return static::$LE;
4049
    }
4050
4051
    /**
4052
     * Set the line break format string, e.g. "\r\n".
4053
     *
4054
     * @param string $le
4055
     */
4056
    protected static function setLE($le)
4057
    {
4058
        static::$LE = $le;
4059
    }
4060
4061
    /**
4062
     * Set the public and private key files and password for S/MIME signing.
4063
     *
4064
     * @param string $cert_filename
4065
     * @param string $key_filename
4066
     * @param string $key_pass            Password for private key
4067
     * @param string $extracerts_filename Optional path to chain certificate
4068
     */
4069
    public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
4070
    {
4071
        $this->sign_cert_file = $cert_filename;
4072
        $this->sign_key_file = $key_filename;
4073
        $this->sign_key_pass = $key_pass;
4074
        $this->sign_extracerts_file = $extracerts_filename;
4075
    }
4076
4077
    /**
4078
     * Quoted-Printable-encode a DKIM header.
4079
     *
4080
     * @param string $txt
4081
     *
4082
     * @return string
4083
     */
4084
    public function DKIM_QP($txt)
4085
    {
4086
        $line = '';
4087
        $len = strlen($txt);
4088
        for ($i = 0; $i < $len; ++$i) {
4089
            $ord = ord($txt[$i]);
4090
            if (((0x21 <= $ord) and ($ord <= 0x3A)) or $ord == 0x3C or ((0x3E <= $ord) and ($ord <= 0x7E))) {
4091
                $line .= $txt[$i];
4092
            } else {
4093
                $line .= '=' . sprintf('%02X', $ord);
4094
            }
4095
        }
4096
4097
        return $line;
4098
    }
4099
4100
    /**
4101
     * Generate a DKIM signature.
4102
     *
4103
     * @param string $signHeader
4104
     *
4105
     * @throws Exception
4106
     *
4107
     * @return string The DKIM signature value
4108
     */
4109
    public function DKIM_Sign($signHeader)
4110
    {
4111
        if (!defined('PKCS7_TEXT')) {
4112
            if ($this->exceptions) {
4113
                throw new Exception($this->lang('extension_missing') . 'openssl');
4114
            }
4115
4116
            return '';
4117
        }
4118
        $privKeyStr = !empty($this->DKIM_private_string) ?
4119
            $this->DKIM_private_string :
4120
            file_get_contents($this->DKIM_private);
4121
        if ('' != $this->DKIM_passphrase) {
4122
            $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
4123
        } else {
4124
            $privKey = openssl_pkey_get_private($privKeyStr);
4125
        }
4126
        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
4127
            openssl_pkey_free($privKey);
4128
4129
            return base64_encode($signature);
4130
        }
4131
        openssl_pkey_free($privKey);
4132
4133
        return '';
4134
    }
4135
4136
    /**
4137
     * Generate a DKIM canonicalization header.
4138
     * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
4139
     *
4140
     * @see    https://tools.ietf.org/html/rfc6376#section-3.4.2
4141
     *
4142
     * @param string $signHeader Header
4143
     *
4144
     * @return string
4145
     */
4146
    public function DKIM_HeaderC($signHeader)
4147
    {
4148
        //Unfold all header continuation lines
4149
        //Also collapses folded whitespace.
4150
        //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
4151
        //@see https://tools.ietf.org/html/rfc5322#section-2.2
4152
        //That means this may break if you do something daft like put vertical tabs in your headers.
4153
        $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
4154
        $lines = explode("\r\n", $signHeader);
4155
        foreach ($lines as $key => $line) {
4156
            //If the header is missing a :, skip it as it's invalid
4157
            //This is likely to happen because the explode() above will also split
4158
            //on the trailing LE, leaving an empty line
4159
            if (strpos($line, ':') === false) {
4160
                continue;
4161
            }
4162
            list($heading, $value) = explode(':', $line, 2);
4163
            //Lower-case header name
4164
            $heading = strtolower($heading);
4165
            //Collapse white space within the value
4166
            $value = preg_replace('/[ \t]{2,}/', ' ', $value);
4167
            //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
4168
            //But then says to delete space before and after the colon.
4169
            //Net result is the same as trimming both ends of the value.
4170
            //by elimination, the same applies to the field name
4171
            $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
4172
        }
4173
4174
        return implode(static::$LE, $lines);
4175
    }
4176
4177
    /**
4178
     * Generate a DKIM canonicalization body.
4179
     * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
4180
     *
4181
     * @see    https://tools.ietf.org/html/rfc6376#section-3.4.3
4182
     *
4183
     * @param string $body Message Body
4184
     *
4185
     * @return string
4186
     */
4187
    public function DKIM_BodyC($body)
4188
    {
4189
        if (empty($body)) {
4190
            return static::$LE;
4191
        }
4192
        // Normalize line endings
4193
        $body = static::normalizeBreaks($body);
4194
4195
        //Reduce multiple trailing line breaks to a single one
4196
        return rtrim($body, "\r\n") . static::$LE;
4197
    }
4198
4199
    /**
4200
     * Create the DKIM header and body in a new message header.
4201
     *
4202
     * @param string $headers_line Header lines
4203
     * @param string $subject      Subject
4204
     * @param string $body         Body
4205
     *
4206
     * @return string
4207
     */
4208
    public function DKIM_Add($headers_line, $subject, $body)
4209
    {
4210
        $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms
4211
        $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body
4212
        $DKIMquery = 'dns/txt'; // Query method
4213
        $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone)
4214
        $subject_header = "Subject: $subject";
4215
        $headers = explode(static::$LE, $headers_line);
4216
        $from_header = '';
4217
        $to_header = '';
4218
        $date_header = '';
4219
        $current = '';
4220
        foreach ($headers as $header) {
4221
            if (strpos($header, 'From:') === 0) {
4222
                $from_header = $header;
4223
                $current = 'from_header';
4224
            } elseif (strpos($header, 'To:') === 0) {
4225
                $to_header = $header;
4226
                $current = 'to_header';
4227
            } elseif (strpos($header, 'Date:') === 0) {
4228
                $date_header = $header;
4229
                $current = 'date_header';
4230
            } else {
4231
                if (!empty($$current) and strpos($header, ' =?') === 0) {
4232
                    $$current .= $header;
4233
                } else {
4234
                    $current = '';
4235
                }
4236
            }
4237
        }
4238
        $from = str_replace('|', '=7C', $this->DKIM_QP($from_header));
4239
        $to = str_replace('|', '=7C', $this->DKIM_QP($to_header));
4240
        $date = str_replace('|', '=7C', $this->DKIM_QP($date_header));
4241
        $subject = str_replace(
4242
            '|',
4243
            '=7C',
4244
            $this->DKIM_QP($subject_header)
4245
        ); // Copied header fields (dkim-quoted-printable)
4246
        $body = $this->DKIM_BodyC($body);
4247
        $DKIMlen = strlen($body); // Length of body
4248
        $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body
4249
        if ('' == $this->DKIM_identity) {
4250
            $ident = '';
4251
        } else {
4252
            $ident = ' i=' . $this->DKIM_identity . ';';
4253
        }
4254
        $dkimhdrs = 'DKIM-Signature: v=1; a=' .
4255
            $DKIMsignatureType . '; q=' .
4256
            $DKIMquery . '; l=' .
4257
            $DKIMlen . '; s=' .
4258
            $this->DKIM_selector .
4259
            ";\r\n" .
4260
            "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" .
4261
            "\th=From:To:Date:Subject;\r\n" .
4262
            "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" .
4263
            "\tz=$from\r\n" .
4264
            "\t|$to\r\n" .
4265
            "\t|$date\r\n" .
4266
            "\t|$subject;\r\n" .
4267
            "\tbh=" . $DKIMb64 . ";\r\n" .
4268
            "\tb=";
4269
        $toSign = $this->DKIM_HeaderC(
4270
            $from_header . "\r\n" .
4271
            $to_header . "\r\n" .
4272
            $date_header . "\r\n" .
4273
            $subject_header . "\r\n" .
4274
            $dkimhdrs
4275
        );
4276
        $signed = $this->DKIM_Sign($toSign);
4277
4278
        return static::normalizeBreaks($dkimhdrs . $signed) . static::$LE;
4279
    }
4280
4281
    /**
4282
     * Detect if a string contains a line longer than the maximum line length
4283
     * allowed by RFC 2822 section 2.1.1.
4284
     *
4285
     * @param string $str
4286
     *
4287
     * @return bool
4288
     */
4289
    public static function hasLineLongerThanMax($str)
4290
    {
4291
        return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
4292
    }
4293
4294
    /**
4295
     * Allows for public read access to 'to' property.
4296
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4297
     *
4298
     * @return array
4299
     */
4300
    public function getToAddresses()
4301
    {
4302
        return $this->to;
4303
    }
4304
4305
    /**
4306
     * Allows for public read access to 'cc' property.
4307
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4308
     *
4309
     * @return array
4310
     */
4311
    public function getCcAddresses()
4312
    {
4313
        return $this->cc;
4314
    }
4315
4316
    /**
4317
     * Allows for public read access to 'bcc' property.
4318
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4319
     *
4320
     * @return array
4321
     */
4322
    public function getBccAddresses()
4323
    {
4324
        return $this->bcc;
4325
    }
4326
4327
    /**
4328
     * Allows for public read access to 'ReplyTo' property.
4329
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4330
     *
4331
     * @return array
4332
     */
4333
    public function getReplyToAddresses()
4334
    {
4335
        return $this->ReplyTo;
4336
    }
4337
4338
    /**
4339
     * Allows for public read access to 'all_recipients' property.
4340
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4341
     *
4342
     * @return array
4343
     */
4344
    public function getAllRecipientAddresses()
4345
    {
4346
        return $this->all_recipients;
4347
    }
4348
4349
    /**
4350
     * Perform a callback.
4351
     *
4352
     * @param bool   $isSent
4353
     * @param array  $to
4354
     * @param array  $cc
4355
     * @param array  $bcc
4356
     * @param string $subject
4357
     * @param string $body
4358
     * @param string $from
4359
     * @param array  $extra
4360
     */
4361
    protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
4362
    {
4363
        if (!empty($this->action_function) and is_callable($this->action_function)) {
4364
            call_user_func_array($this->action_function, [$isSent, $to, $cc, $bcc, $subject, $body, $from, $extra]);
4365
        }
4366
    }
4367
4368
    /**
4369
     * Get the OAuth instance.
4370
     *
4371
     * @return OAuth
4372
     */
4373
    public function getOAuth()
4374
    {
4375
        return $this->oauth;
4376
    }
4377
4378
    /**
4379
     * Set an OAuth instance.
4380
     *
4381
     * @param OAuth $oauth
4382
     */
4383
    public function setOAuth(OAuth $oauth)
4384
    {
4385
        $this->oauth = $oauth;
4386
    }
4387
}
4388