PHPMailer::doCallback()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 8
dl 0
loc 6
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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;
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;
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;
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.5';
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
     * This length does NOT include the line break
712
     * 76 means that lines will be 77 or 78 chars depending on whether
713
     * the line break format is LF or CRLF; both are valid.
714
     *
715
     * @var int
716
     */
717
    const STD_LINE_LENGTH = 76;
718
719
    /**
720
     * Constructor.
721
     *
722
     * @param bool $exceptions Should we throw external exceptions?
0 ignored issues
show
Documentation introduced by
Should the type for parameter $exceptions not be boolean|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
723
     */
724
    public function __construct($exceptions = null)
725
    {
726
        if (null !== $exceptions) {
727
            $this->exceptions = (bool) $exceptions;
728
        }
729
        //Pick an appropriate debug output format automatically
730
        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
731
    }
732
733
    /**
734
     * Destructor.
735
     */
736
    public function __destruct()
737
    {
738
        //Close any open SMTP connection nicely
739
        $this->smtpClose();
740
    }
741
742
    /**
743
     * Call mail() in a safe_mode-aware fashion.
744
     * Also, unless sendmail_path points to sendmail (or something that
745
     * claims to be sendmail), don't pass params (not a perfect fix,
746
     * but it will do).
747
     *
748
     * @param string      $to      To
749
     * @param string      $subject Subject
750
     * @param string      $body    Message Body
751
     * @param string      $header  Additional Header(s)
752
     * @param string|null $params  Params
753
     *
754
     * @return bool
755
     */
756
    private function mailPassthru($to, $subject, $body, $header, $params)
757
    {
758
        //Check overloading of mail function to avoid double-encoding
759
        if (ini_get('mbstring.func_overload') & 1) {
760
            $subject = $this->secureHeader($subject);
761
        } else {
762
            $subject = $this->encodeHeader($this->secureHeader($subject));
763
        }
764
        //Calling mail() with null params breaks
765
        if (!$this->UseSendmailOptions or null === $params) {
766
            $result = @mail($to, $subject, $body, $header);
767
        } else {
768
            $result = @mail($to, $subject, $body, $header, $params);
769
        }
770
771
        return $result;
772
    }
773
774
    /**
775
     * Output debugging info via user-defined method.
776
     * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug).
777
     *
778
     * @see PHPMailer::$Debugoutput
779
     * @see PHPMailer::$SMTPDebug
780
     *
781
     * @param string $str
782
     */
783
    protected function edebug($str)
784
    {
785
        if ($this->SMTPDebug <= 0) {
786
            return;
787
        }
788
        //Is this a PSR-3 logger?
789
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
790
            $this->Debugoutput->debug($str);
791
792
            return;
793
        }
794
        //Avoid clash with built-in function names
795 View Code Duplication
        if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) {
796
            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
797
798
            return;
799
        }
800 View Code Duplication
        switch ($this->Debugoutput) {
801
            case 'error_log':
802
                //Don't output, just log
803
                error_log($str);
804
                break;
805
            case 'html':
806
                //Cleans up output a bit for a better looking, HTML-safe output
807
                echo htmlentities(
808
                    preg_replace('/[\r\n]+/', '', $str),
809
                    ENT_QUOTES,
810
                    'UTF-8'
811
                ), "<br>\n";
812
                break;
813
            case 'echo':
814
            default:
815
                //Normalize line breaks
816
                $str = preg_replace('/\r\n|\r/ms', "\n", $str);
817
                echo gmdate('Y-m-d H:i:s'),
818
                "\t",
819
                    //Trim trailing space
820
                trim(
821
                //Indent for readability, except for trailing break
822
                    str_replace(
823
                        "\n",
824
                        "\n                   \t                  ",
825
                        trim($str)
826
                    )
827
                ),
828
                "\n";
829
        }
830
    }
831
832
    /**
833
     * Sets message type to HTML or plain.
834
     *
835
     * @param bool $isHtml True for HTML mode
836
     */
837
    public function isHTML($isHtml = true)
838
    {
839
        if ($isHtml) {
840
            $this->ContentType = 'text/html';
841
        } else {
842
            $this->ContentType = 'text/plain';
843
        }
844
    }
845
846
    /**
847
     * Send messages using SMTP.
848
     */
849
    public function isSMTP()
850
    {
851
        $this->Mailer = 'smtp';
852
    }
853
854
    /**
855
     * Send messages using PHP's mail() function.
856
     */
857
    public function isMail()
858
    {
859
        $this->Mailer = 'mail';
860
    }
861
862
    /**
863
     * Send messages using $Sendmail.
864
     */
865 View Code Duplication
    public function isSendmail()
866
    {
867
        $ini_sendmail_path = ini_get('sendmail_path');
868
869
        if (false === stripos($ini_sendmail_path, 'sendmail')) {
870
            $this->Sendmail = '/usr/sbin/sendmail';
871
        } else {
872
            $this->Sendmail = $ini_sendmail_path;
873
        }
874
        $this->Mailer = 'sendmail';
875
    }
876
877
    /**
878
     * Send messages using qmail.
879
     */
880 View Code Duplication
    public function isQmail()
881
    {
882
        $ini_sendmail_path = ini_get('sendmail_path');
883
884
        if (false === stripos($ini_sendmail_path, 'qmail')) {
885
            $this->Sendmail = '/var/qmail/bin/qmail-inject';
886
        } else {
887
            $this->Sendmail = $ini_sendmail_path;
888
        }
889
        $this->Mailer = 'qmail';
890
    }
891
892
    /**
893
     * Add a "To" address.
894
     *
895
     * @param string $address The email address to send to
896
     * @param string $name
897
     *
898
     * @return bool true on success, false if address already used or invalid in some way
899
     */
900
    public function addAddress($address, $name = '')
901
    {
902
        return $this->addOrEnqueueAnAddress('to', $address, $name);
903
    }
904
905
    /**
906
     * Add a "CC" address.
907
     *
908
     * @param string $address The email address to send to
909
     * @param string $name
910
     *
911
     * @return bool true on success, false if address already used or invalid in some way
912
     */
913
    public function addCC($address, $name = '')
914
    {
915
        return $this->addOrEnqueueAnAddress('cc', $address, $name);
916
    }
917
918
    /**
919
     * Add a "BCC" address.
920
     *
921
     * @param string $address The email address to send to
922
     * @param string $name
923
     *
924
     * @return bool true on success, false if address already used or invalid in some way
925
     */
926
    public function addBCC($address, $name = '')
927
    {
928
        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
929
    }
930
931
    /**
932
     * Add a "Reply-To" address.
933
     *
934
     * @param string $address The email address to reply to
935
     * @param string $name
936
     *
937
     * @return bool true on success, false if address already used or invalid in some way
938
     */
939
    public function addReplyTo($address, $name = '')
940
    {
941
        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
942
    }
943
944
    /**
945
     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
946
     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
947
     * be modified after calling this function), addition of such addresses is delayed until send().
948
     * Addresses that have been added already return false, but do not throw exceptions.
949
     *
950
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
951
     * @param string $address The email address to send, resp. to reply to
952
     * @param string $name
953
     *
954
     * @throws Exception
955
     *
956
     * @return bool true on success, false if address already used or invalid in some way
957
     */
958
    protected function addOrEnqueueAnAddress($kind, $address, $name)
959
    {
960
        $address = trim($address);
961
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
962
        $pos = strrpos($address, '@');
963
        if (false === $pos) {
964
            // At-sign is missing.
965
            $error_message = sprintf('%s (%s): %s',
966
                $this->lang('invalid_address'),
967
                $kind,
968
                $address);
969
            $this->setError($error_message);
970
            $this->edebug($error_message);
971
            if ($this->exceptions) {
972
                throw new Exception($error_message);
973
            }
974
975
            return false;
976
        }
977
        $params = [$kind, $address, $name];
978
        // Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
979
        if ($this->has8bitChars(substr($address, ++$pos)) and static::idnSupported()) {
980
            if ('Reply-To' != $kind) {
981
                if (!array_key_exists($address, $this->RecipientsQueue)) {
982
                    $this->RecipientsQueue[$address] = $params;
983
984
                    return true;
985
                }
986
            } else {
987
                if (!array_key_exists($address, $this->ReplyToQueue)) {
988
                    $this->ReplyToQueue[$address] = $params;
989
990
                    return true;
991
                }
992
            }
993
994
            return false;
995
        }
996
997
        // Immediately add standard addresses without IDN.
998
        return call_user_func_array([$this, 'addAnAddress'], $params);
999
    }
1000
1001
    /**
1002
     * Add an address to one of the recipient arrays or to the ReplyTo array.
1003
     * Addresses that have been added already return false, but do not throw exceptions.
1004
     *
1005
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1006
     * @param string $address The email address to send, resp. to reply to
1007
     * @param string $name
1008
     *
1009
     * @throws Exception
1010
     *
1011
     * @return bool true on success, false if address already used or invalid in some way
1012
     */
1013
    protected function addAnAddress($kind, $address, $name = '')
1014
    {
1015
        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1016
            $error_message = sprintf('%s: %s',
1017
                $this->lang('Invalid recipient kind'),
1018
                $kind);
1019
            $this->setError($error_message);
1020
            $this->edebug($error_message);
1021
            if ($this->exceptions) {
1022
                throw new Exception($error_message);
1023
            }
1024
1025
            return false;
1026
        }
1027 View Code Duplication
        if (!static::validateAddress($address)) {
1028
            $error_message = sprintf('%s (%s): %s',
1029
                $this->lang('invalid_address'),
1030
                $kind,
1031
                $address);
1032
            $this->setError($error_message);
1033
            $this->edebug($error_message);
1034
            if ($this->exceptions) {
1035
                throw new Exception($error_message);
1036
            }
1037
1038
            return false;
1039
        }
1040
        if ('Reply-To' != $kind) {
1041 View Code Duplication
            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
1042
                $this->{$kind}[] = [$address, $name];
1043
                $this->all_recipients[strtolower($address)] = true;
1044
1045
                return true;
1046
            }
1047 View Code Duplication
        } else {
1048
            if (!array_key_exists(strtolower($address), $this->ReplyTo)) {
1049
                $this->ReplyTo[strtolower($address)] = [$address, $name];
1050
1051
                return true;
1052
            }
1053
        }
1054
1055
        return false;
1056
    }
1057
1058
    /**
1059
     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1060
     * of the form "display name <address>" into an array of name/address pairs.
1061
     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1062
     * Note that quotes in the name part are removed.
1063
     *
1064
     * @see    http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1065
     *
1066
     * @param string $addrstr The address list string
1067
     * @param bool   $useimap Whether to use the IMAP extension to parse the list
1068
     *
1069
     * @return array
1070
     */
1071
    public static function parseAddresses($addrstr, $useimap = true)
1072
    {
1073
        $addresses = [];
1074
        if ($useimap and function_exists('imap_rfc822_parse_adrlist')) {
1075
            //Use this built-in parser if it's available
1076
            $list = imap_rfc822_parse_adrlist($addrstr, '');
1077
            foreach ($list as $address) {
1078
                if ('.SYNTAX-ERROR.' != $address->host) {
1079
                    if (static::validateAddress($address->mailbox . '@' . $address->host)) {
1080
                        $addresses[] = [
1081
                            'name' => (property_exists($address, 'personal') ? $address->personal : ''),
1082
                            'address' => $address->mailbox . '@' . $address->host,
1083
                        ];
1084
                    }
1085
                }
1086
            }
1087
        } else {
1088
            //Use this simpler parser
1089
            $list = explode(',', $addrstr);
1090
            foreach ($list as $address) {
1091
                $address = trim($address);
1092
                //Is there a separate name part?
1093
                if (strpos($address, '<') === false) {
1094
                    //No separate name, just use the whole thing
1095
                    if (static::validateAddress($address)) {
1096
                        $addresses[] = [
1097
                            'name' => '',
1098
                            'address' => $address,
1099
                        ];
1100
                    }
1101
                } else {
1102
                    list($name, $email) = explode('<', $address);
1103
                    $email = trim(str_replace('>', '', $email));
1104
                    if (static::validateAddress($email)) {
1105
                        $addresses[] = [
1106
                            'name' => trim(str_replace(['"', "'"], '', $name)),
1107
                            'address' => $email,
1108
                        ];
1109
                    }
1110
                }
1111
            }
1112
        }
1113
1114
        return $addresses;
1115
    }
1116
1117
    /**
1118
     * Set the From and FromName properties.
1119
     *
1120
     * @param string $address
1121
     * @param string $name
1122
     * @param bool   $auto    Whether to also set the Sender address, defaults to true
1123
     *
1124
     * @throws Exception
1125
     *
1126
     * @return bool
1127
     */
1128
    public function setFrom($address, $name = '', $auto = true)
1129
    {
1130
        $address = trim($address);
1131
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1132
        // Don't validate now addresses with IDN. Will be done in send().
1133
        $pos = strrpos($address, '@');
1134
        if (false === $pos or
1135
            (!$this->has8bitChars(substr($address, ++$pos)) or !static::idnSupported()) and
1136
            !static::validateAddress($address)) {
1137
            $error_message = sprintf('%s (From): %s',
1138
                $this->lang('invalid_address'),
1139
                $address);
1140
            $this->setError($error_message);
1141
            $this->edebug($error_message);
1142
            if ($this->exceptions) {
1143
                throw new Exception($error_message);
1144
            }
1145
1146
            return false;
1147
        }
1148
        $this->From = $address;
1149
        $this->FromName = $name;
1150
        if ($auto) {
1151
            if (empty($this->Sender)) {
1152
                $this->Sender = $address;
1153
            }
1154
        }
1155
1156
        return true;
1157
    }
1158
1159
    /**
1160
     * Return the Message-ID header of the last email.
1161
     * Technically this is the value from the last time the headers were created,
1162
     * but it's also the message ID of the last sent message except in
1163
     * pathological cases.
1164
     *
1165
     * @return string
1166
     */
1167
    public function getLastMessageID()
1168
    {
1169
        return $this->lastMessageID;
1170
    }
1171
1172
    /**
1173
     * Check that a string looks like an email address.
1174
     * Validation patterns supported:
1175
     * * `auto` Pick best pattern automatically;
1176
     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
1177
     * * `pcre` Use old PCRE implementation;
1178
     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
1179
     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
1180
     * * `noregex` Don't use a regex: super fast, really dumb.
1181
     * Alternatively you may pass in a callable to inject your own validator, for example:
1182
     *
1183
     * ```php
1184
     * PHPMailer::validateAddress('[email protected]', function($address) {
1185
     *     return (strpos($address, '@') !== false);
1186
     * });
1187
     * ```
1188
     *
1189
     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
1190
     *
1191
     * @param string          $address       The email address to check
1192
     * @param string|callable $patternselect Which pattern to use
0 ignored issues
show
Documentation introduced by
Should the type for parameter $patternselect not be callable|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

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

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1802
     *
1803
     * @throws Exception
1804
     *
1805
     * @uses \PHPMailer\PHPMailer\SMTP
1806
     *
1807
     * @return bool
1808
     */
1809
    public function smtpConnect($options = null)
1810
    {
1811
        if (null === $this->smtp) {
1812
            $this->smtp = $this->getSMTPInstance();
1813
        }
1814
1815
        //If no options are provided, use whatever is set in the instance
1816
        if (null === $options) {
1817
            $options = $this->SMTPOptions;
1818
        }
1819
1820
        // Already connected?
1821
        if ($this->smtp->connected()) {
1822
            return true;
1823
        }
1824
1825
        $this->smtp->setTimeout($this->Timeout);
1826
        $this->smtp->setDebugLevel($this->SMTPDebug);
1827
        $this->smtp->setDebugOutput($this->Debugoutput);
1828
        $this->smtp->setVerp($this->do_verp);
1829
        $hosts = explode(';', $this->Host);
1830
        $lastexception = null;
1831
1832
        foreach ($hosts as $hostentry) {
1833
            $hostinfo = [];
1834 View Code Duplication
            if (!preg_match(
1835
                '/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*|\[[a-fA-F0-9:]+\]):?([0-9]*)$/',
1836
                trim($hostentry),
1837
                $hostinfo
1838
            )) {
1839
                static::edebug($this->lang('connect_host') . ' ' . $hostentry);
1840
                // Not a valid host entry
1841
                continue;
1842
            }
1843
            // $hostinfo[2]: optional ssl or tls prefix
1844
            // $hostinfo[3]: the hostname
1845
            // $hostinfo[4]: optional port number
1846
            // The host string prefix can temporarily override the current setting for SMTPSecure
1847
            // If it's not specified, the default value is used
1848
1849
            //Check the host name is a valid name or IP address before trying to use it
1850 View Code Duplication
            if (!static::isValidHost($hostinfo[3])) {
1851
                static::edebug($this->lang('connect_host') . ' ' . $hostentry);
1852
                continue;
1853
            }
1854
            $prefix = '';
1855
            $secure = $this->SMTPSecure;
1856
            $tls = ('tls' == $this->SMTPSecure);
1857
            if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) {
1858
                $prefix = 'ssl://';
1859
                $tls = false; // Can't have SSL and TLS at the same time
1860
                $secure = 'ssl';
1861
            } elseif ('tls' == $hostinfo[2]) {
1862
                $tls = true;
1863
                // tls doesn't use a prefix
1864
                $secure = 'tls';
1865
            }
1866
            //Do we need the OpenSSL extension?
1867
            $sslext = defined('OPENSSL_ALGO_SHA256');
1868
            if ('tls' === $secure or 'ssl' === $secure) {
1869
                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
1870
                if (!$sslext) {
1871
                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
1872
                }
1873
            }
1874
            $host = $hostinfo[3];
1875
            $port = $this->Port;
1876
            $tport = (int) $hostinfo[4];
1877
            if ($tport > 0 and $tport < 65536) {
1878
                $port = $tport;
1879
            }
1880
            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
1881
                try {
1882
                    if ($this->Helo) {
1883
                        $hello = $this->Helo;
1884
                    } else {
1885
                        $hello = $this->serverHostname();
1886
                    }
1887
                    $this->smtp->hello($hello);
1888
                    //Automatically enable TLS encryption if:
1889
                    // * it's not disabled
1890
                    // * we have openssl extension
1891
                    // * we are not already using SSL
1892
                    // * the server offers STARTTLS
1893
                    if ($this->SMTPAutoTLS and $sslext and 'ssl' != $secure and $this->smtp->getServerExt('STARTTLS')) {
1894
                        $tls = true;
1895
                    }
1896
                    if ($tls) {
1897
                        if (!$this->smtp->startTLS()) {
1898
                            throw new Exception($this->lang('connect_host'));
1899
                        }
1900
                        // We must resend EHLO after TLS negotiation
1901
                        $this->smtp->hello($hello);
1902
                    }
1903
                    if ($this->SMTPAuth) {
1904
                        if (!$this->smtp->authenticate(
1905
                            $this->Username,
1906
                            $this->Password,
1907
                            $this->AuthType,
1908
                            $this->oauth
1909
                        )
1910
                        ) {
1911
                            throw new Exception($this->lang('authenticate'));
1912
                        }
1913
                    }
1914
1915
                    return true;
1916
                } catch (Exception $exc) {
1917
                    $lastexception = $exc;
1918
                    $this->edebug($exc->getMessage());
1919
                    // We must have connected, but then failed TLS or Auth, so close connection nicely
1920
                    $this->smtp->quit();
1921
                }
1922
            }
1923
        }
1924
        // If we get here, all connection attempts have failed, so close connection hard
1925
        $this->smtp->close();
1926
        // As we've caught all exceptions, just report whatever the last one was
1927
        if ($this->exceptions and null !== $lastexception) {
1928
            throw $lastexception;
1929
        }
1930
1931
        return false;
1932
    }
1933
1934
    /**
1935
     * Close the active SMTP session if one exists.
1936
     */
1937
    public function smtpClose()
1938
    {
1939
        if (null !== $this->smtp) {
1940
            if ($this->smtp->connected()) {
1941
                $this->smtp->quit();
1942
                $this->smtp->close();
1943
            }
1944
        }
1945
    }
1946
1947
    /**
1948
     * Set the language for error messages.
1949
     * Returns false if it cannot load the language file.
1950
     * The default language is English.
1951
     *
1952
     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
1953
     * @param string $lang_path Path to the language file directory, with trailing separator (slash)
1954
     *
1955
     * @return bool
1956
     */
1957
    public function setLanguage($langcode = 'en', $lang_path = '')
1958
    {
1959
        // Backwards compatibility for renamed language codes
1960
        $renamed_langcodes = [
1961
            'br' => 'pt_br',
1962
            'cz' => 'cs',
1963
            'dk' => 'da',
1964
            'no' => 'nb',
1965
            'se' => 'sv',
1966
            'sr' => 'rs',
1967
        ];
1968
1969
        if (isset($renamed_langcodes[$langcode])) {
1970
            $langcode = $renamed_langcodes[$langcode];
1971
        }
1972
1973
        // Define full set of translatable strings in English
1974
        $PHPMAILER_LANG = [
1975
            'authenticate' => 'SMTP Error: Could not authenticate.',
1976
            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
1977
            'data_not_accepted' => 'SMTP Error: data not accepted.',
1978
            'empty_message' => 'Message body empty',
1979
            'encoding' => 'Unknown encoding: ',
1980
            'execute' => 'Could not execute: ',
1981
            'file_access' => 'Could not access file: ',
1982
            'file_open' => 'File Error: Could not open file: ',
1983
            'from_failed' => 'The following From address failed: ',
1984
            'instantiate' => 'Could not instantiate mail function.',
1985
            'invalid_address' => 'Invalid address: ',
1986
            'mailer_not_supported' => ' mailer is not supported.',
1987
            'provide_address' => 'You must provide at least one recipient email address.',
1988
            'recipients_failed' => 'SMTP Error: The following recipients failed: ',
1989
            'signing' => 'Signing Error: ',
1990
            'smtp_connect_failed' => 'SMTP connect() failed.',
1991
            'smtp_error' => 'SMTP server error: ',
1992
            'variable_set' => 'Cannot set or reset variable: ',
1993
            'extension_missing' => 'Extension missing: ',
1994
        ];
1995
        if (empty($lang_path)) {
1996
            // Calculate an absolute path so it can work if CWD is not here
1997
            $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
1998
        }
1999
        //Validate $langcode
2000
        if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) {
2001
            $langcode = 'en';
2002
        }
2003
        $foundlang = true;
2004
        $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
2005
        // There is no English translation file
2006
        if ('en' != $langcode) {
2007
            // Make sure language file path is readable
2008
            if (!file_exists($lang_file)) {
2009
                $foundlang = false;
2010
            } else {
2011
                // Overwrite language-specific strings.
2012
                // This way we'll never have missing translation keys.
2013
                $foundlang = include $lang_file;
2014
            }
2015
        }
2016
        $this->language = $PHPMAILER_LANG;
2017
2018
        return (bool) $foundlang; // Returns false if language not found
2019
    }
2020
2021
    /**
2022
     * Get the array of strings for the current language.
2023
     *
2024
     * @return array
2025
     */
2026
    public function getTranslations()
2027
    {
2028
        return $this->language;
2029
    }
2030
2031
    /**
2032
     * Create recipient headers.
2033
     *
2034
     * @param string $type
2035
     * @param array  $addr An array of recipients,
2036
     *                     where each recipient is a 2-element indexed array with element 0 containing an address
2037
     *                     and element 1 containing a name, like:
2038
     *                     [['[email protected]', 'Joe User'], ['[email protected]', 'Zoe User']]
2039
     *
2040
     * @return string
2041
     */
2042
    public function addrAppend($type, $addr)
2043
    {
2044
        $addresses = [];
2045
        foreach ($addr as $address) {
2046
            $addresses[] = $this->addrFormat($address);
2047
        }
2048
2049
        return $type . ': ' . implode(', ', $addresses) . static::$LE;
2050
    }
2051
2052
    /**
2053
     * Format an address for use in a message header.
2054
     *
2055
     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
2056
     *                    ['[email protected]', 'Joe User']
2057
     *
2058
     * @return string
2059
     */
2060
    public function addrFormat($addr)
2061
    {
2062
        if (empty($addr[1])) { // No name provided
2063
            return $this->secureHeader($addr[0]);
2064
        }
2065
2066
        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader(
2067
                $addr[0]
2068
            ) . '>';
2069
    }
2070
2071
    /**
2072
     * Word-wrap message.
2073
     * For use with mailers that do not automatically perform wrapping
2074
     * and for quoted-printable encoded messages.
2075
     * Original written by philippe.
2076
     *
2077
     * @param string $message The message to wrap
2078
     * @param int    $length  The line length to wrap to
2079
     * @param bool   $qp_mode Whether to run in Quoted-Printable mode
2080
     *
2081
     * @return string
2082
     */
2083
    public function wrapText($message, $length, $qp_mode = false)
2084
    {
2085
        if ($qp_mode) {
2086
            $soft_break = sprintf(' =%s', static::$LE);
2087
        } else {
2088
            $soft_break = static::$LE;
2089
        }
2090
        // If utf-8 encoding is used, we will need to make sure we don't
2091
        // split multibyte characters when we wrap
2092
        $is_utf8 = 'utf-8' == strtolower($this->CharSet);
2093
        $lelen = strlen(static::$LE);
2094
        $crlflen = strlen(static::$LE);
2095
2096
        $message = static::normalizeBreaks($message);
2097
        //Remove a trailing line break
2098
        if (substr($message, -$lelen) == static::$LE) {
2099
            $message = substr($message, 0, -$lelen);
2100
        }
2101
2102
        //Split message into lines
2103
        $lines = explode(static::$LE, $message);
2104
        //Message will be rebuilt in here
2105
        $message = '';
2106
        foreach ($lines as $line) {
2107
            $words = explode(' ', $line);
2108
            $buf = '';
2109
            $firstword = true;
2110
            foreach ($words as $word) {
2111
                if ($qp_mode and (strlen($word) > $length)) {
2112
                    $space_left = $length - strlen($buf) - $crlflen;
2113
                    if (!$firstword) {
2114
                        if ($space_left > 20) {
2115
                            $len = $space_left;
2116 View Code Duplication
                            if ($is_utf8) {
2117
                                $len = $this->utf8CharBoundary($word, $len);
2118
                            } elseif ('=' == substr($word, $len - 1, 1)) {
2119
                                --$len;
2120
                            } elseif ('=' == substr($word, $len - 2, 1)) {
2121
                                $len -= 2;
2122
                            }
2123
                            $part = substr($word, 0, $len);
2124
                            $word = substr($word, $len);
2125
                            $buf .= ' ' . $part;
2126
                            $message .= $buf . sprintf('=%s', static::$LE);
2127
                        } else {
2128
                            $message .= $buf . $soft_break;
2129
                        }
2130
                        $buf = '';
2131
                    }
2132
                    while (strlen($word) > 0) {
2133
                        if ($length <= 0) {
2134
                            break;
2135
                        }
2136
                        $len = $length;
2137 View Code Duplication
                        if ($is_utf8) {
2138
                            $len = $this->utf8CharBoundary($word, $len);
2139
                        } elseif ('=' == substr($word, $len - 1, 1)) {
2140
                            --$len;
2141
                        } elseif ('=' == substr($word, $len - 2, 1)) {
2142
                            $len -= 2;
2143
                        }
2144
                        $part = substr($word, 0, $len);
2145
                        $word = substr($word, $len);
2146
2147
                        if (strlen($word) > 0) {
2148
                            $message .= $part . sprintf('=%s', static::$LE);
2149
                        } else {
2150
                            $buf = $part;
2151
                        }
2152
                    }
2153
                } else {
2154
                    $buf_o = $buf;
2155
                    if (!$firstword) {
2156
                        $buf .= ' ';
2157
                    }
2158
                    $buf .= $word;
2159
2160
                    if (strlen($buf) > $length and '' != $buf_o) {
2161
                        $message .= $buf_o . $soft_break;
2162
                        $buf = $word;
2163
                    }
2164
                }
2165
                $firstword = false;
2166
            }
2167
            $message .= $buf . static::$LE;
2168
        }
2169
2170
        return $message;
2171
    }
2172
2173
    /**
2174
     * Find the last character boundary prior to $maxLength in a utf-8
2175
     * quoted-printable encoded string.
2176
     * Original written by Colin Brown.
2177
     *
2178
     * @param string $encodedText utf-8 QP text
2179
     * @param int    $maxLength   Find the last character boundary prior to this length
2180
     *
2181
     * @return int
2182
     */
2183
    public function utf8CharBoundary($encodedText, $maxLength)
2184
    {
2185
        $foundSplitPos = false;
2186
        $lookBack = 3;
2187
        while (!$foundSplitPos) {
2188
            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
2189
            $encodedCharPos = strpos($lastChunk, '=');
2190
            if (false !== $encodedCharPos) {
2191
                // Found start of encoded character byte within $lookBack block.
2192
                // Check the encoded byte value (the 2 chars after the '=')
2193
                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
2194
                $dec = hexdec($hex);
2195
                if ($dec < 128) {
2196
                    // Single byte character.
2197
                    // If the encoded char was found at pos 0, it will fit
2198
                    // otherwise reduce maxLength to start of the encoded char
2199
                    if ($encodedCharPos > 0) {
2200
                        $maxLength -= $lookBack - $encodedCharPos;
2201
                    }
2202
                    $foundSplitPos = true;
2203
                } elseif ($dec >= 192) {
2204
                    // First byte of a multi byte character
2205
                    // Reduce maxLength to split at start of character
2206
                    $maxLength -= $lookBack - $encodedCharPos;
2207
                    $foundSplitPos = true;
2208
                } elseif ($dec < 192) {
2209
                    // Middle byte of a multi byte character, look further back
2210
                    $lookBack += 3;
2211
                }
2212
            } else {
2213
                // No encoded character found
2214
                $foundSplitPos = true;
2215
            }
2216
        }
2217
2218
        return $maxLength;
2219
    }
2220
2221
    /**
2222
     * Apply word wrapping to the message body.
2223
     * Wraps the message body to the number of chars set in the WordWrap property.
2224
     * You should only do this to plain-text bodies as wrapping HTML tags may break them.
2225
     * This is called automatically by createBody(), so you don't need to call it yourself.
2226
     */
2227
    public function setWordWrap()
2228
    {
2229
        if ($this->WordWrap < 1) {
2230
            return;
2231
        }
2232
2233
        switch ($this->message_type) {
2234
            case 'alt':
2235
            case 'alt_inline':
2236
            case 'alt_attach':
2237
            case 'alt_inline_attach':
2238
                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
2239
                break;
2240
            default:
2241
                $this->Body = $this->wrapText($this->Body, $this->WordWrap);
2242
                break;
2243
        }
2244
    }
2245
2246
    /**
2247
     * Assemble message headers.
2248
     *
2249
     * @return string The assembled headers
2250
     */
2251
    public function createHeader()
2252
    {
2253
        $result = '';
2254
2255
        $result .= $this->headerLine('Date', '' == $this->MessageDate ? self::rfcDate() : $this->MessageDate);
2256
2257
        // To be created automatically by mail()
2258
        if ($this->SingleTo) {
2259
            if ('mail' != $this->Mailer) {
2260
                foreach ($this->to as $toaddr) {
2261
                    $this->SingleToArray[] = $this->addrFormat($toaddr);
2262
                }
2263
            }
2264
        } else {
2265
            if (count($this->to) > 0) {
2266
                if ('mail' != $this->Mailer) {
2267
                    $result .= $this->addrAppend('To', $this->to);
2268
                }
2269
            } elseif (count($this->cc) == 0) {
2270
                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
2271
            }
2272
        }
2273
2274
        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
2275
2276
        // sendmail and mail() extract Cc from the header before sending
2277
        if (count($this->cc) > 0) {
2278
            $result .= $this->addrAppend('Cc', $this->cc);
2279
        }
2280
2281
        // sendmail and mail() extract Bcc from the header before sending
2282
        if ((
2283
                'sendmail' == $this->Mailer or 'qmail' == $this->Mailer or 'mail' == $this->Mailer
2284
            )
2285
            and count($this->bcc) > 0
2286
        ) {
2287
            $result .= $this->addrAppend('Bcc', $this->bcc);
2288
        }
2289
2290
        if (count($this->ReplyTo) > 0) {
2291
            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
2292
        }
2293
2294
        // mail() sets the subject itself
2295
        if ('mail' != $this->Mailer) {
2296
            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
2297
        }
2298
2299
        // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
2300
        // https://tools.ietf.org/html/rfc5322#section-3.6.4
2301
        if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) {
2302
            $this->lastMessageID = $this->MessageID;
2303
        } else {
2304
            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
2305
        }
2306
        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
2307
        if (null !== $this->Priority) {
2308
            $result .= $this->headerLine('X-Priority', $this->Priority);
2309
        }
2310
        if ('' == $this->XMailer) {
2311
            $result .= $this->headerLine(
2312
                'X-Mailer',
2313
                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
2314
            );
2315
        } else {
2316
            $myXmailer = trim($this->XMailer);
2317
            if ($myXmailer) {
2318
                $result .= $this->headerLine('X-Mailer', $myXmailer);
2319
            }
2320
        }
2321
2322
        if ('' != $this->ConfirmReadingTo) {
2323
            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
2324
        }
2325
2326
        // Add custom headers
2327
        foreach ($this->CustomHeader as $header) {
2328
            $result .= $this->headerLine(
2329
                trim($header[0]),
2330
                $this->encodeHeader(trim($header[1]))
2331
            );
2332
        }
2333
        if (!$this->sign_key_file) {
2334
            $result .= $this->headerLine('MIME-Version', '1.0');
2335
            $result .= $this->getMailMIME();
2336
        }
2337
2338
        return $result;
2339
    }
2340
2341
    /**
2342
     * Get the message MIME type headers.
2343
     *
2344
     * @return string
2345
     */
2346
    public function getMailMIME()
2347
    {
2348
        $result = '';
2349
        $ismultipart = true;
2350
        switch ($this->message_type) {
2351 View Code Duplication
            case 'inline':
2352
                $result .= $this->headerLine('Content-Type', 'multipart/related;');
2353
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2354
                break;
2355
            case 'attach':
2356
            case 'inline_attach':
2357
            case 'alt_attach':
2358 View Code Duplication
            case 'alt_inline_attach':
2359
                $result .= $this->headerLine('Content-Type', 'multipart/mixed;');
2360
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2361
                break;
2362
            case 'alt':
2363 View Code Duplication
            case 'alt_inline':
2364
                $result .= $this->headerLine('Content-Type', 'multipart/alternative;');
2365
                $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"');
2366
                break;
2367
            default:
2368
                // Catches case 'plain': and case '':
2369
                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
2370
                $ismultipart = false;
2371
                break;
2372
        }
2373
        // RFC1341 part 5 says 7bit is assumed if not specified
2374
        if ('7bit' != $this->Encoding) {
2375
            // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
2376
            if ($ismultipart) {
2377
                if ('8bit' == $this->Encoding) {
2378
                    $result .= $this->headerLine('Content-Transfer-Encoding', '8bit');
2379
                }
2380
                // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
2381
            } else {
2382
                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
2383
            }
2384
        }
2385
2386
        if ('mail' != $this->Mailer) {
2387
            $result .= static::$LE;
2388
        }
2389
2390
        return $result;
2391
    }
2392
2393
    /**
2394
     * Returns the whole MIME message.
2395
     * Includes complete headers and body.
2396
     * Only valid post preSend().
2397
     *
2398
     * @see PHPMailer::preSend()
2399
     *
2400
     * @return string
2401
     */
2402
    public function getSentMIMEMessage()
2403
    {
2404
        return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . static::$LE . static::$LE . $this->MIMEBody;
2405
    }
2406
2407
    /**
2408
     * Create a unique ID to use for boundaries.
2409
     *
2410
     * @return string
2411
     */
2412
    protected function generateId()
2413
    {
2414
        $len = 32; //32 bytes = 256 bits
2415
        if (function_exists('random_bytes')) {
2416
            $bytes = random_bytes($len);
2417
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
2418
            $bytes = openssl_random_pseudo_bytes($len);
2419
        } else {
2420
            //Use a hash to force the length to the same as the other methods
2421
            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
2422
        }
2423
2424
        //We don't care about messing up base64 format here, just want a random string
2425
        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
2426
    }
2427
2428
    /**
2429
     * Assemble the message body.
2430
     * Returns an empty string on failure.
2431
     *
2432
     * @throws Exception
2433
     *
2434
     * @return string The assembled message body
2435
     */
2436
    public function createBody()
2437
    {
2438
        $body = '';
2439
        //Create unique IDs and preset boundaries
2440
        $this->uniqueid = $this->generateId();
2441
        $this->boundary[1] = 'b1_' . $this->uniqueid;
2442
        $this->boundary[2] = 'b2_' . $this->uniqueid;
2443
        $this->boundary[3] = 'b3_' . $this->uniqueid;
2444
2445
        if ($this->sign_key_file) {
2446
            $body .= $this->getMailMIME() . static::$LE;
2447
        }
2448
2449
        $this->setWordWrap();
2450
2451
        $bodyEncoding = $this->Encoding;
2452
        $bodyCharSet = $this->CharSet;
2453
        //Can we do a 7-bit downgrade?
2454
        if ('8bit' == $bodyEncoding and !$this->has8bitChars($this->Body)) {
2455
            $bodyEncoding = '7bit';
2456
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2457
            $bodyCharSet = 'us-ascii';
2458
        }
2459
        //If lines are too long, and we're not already using an encoding that will shorten them,
2460
        //change to quoted-printable transfer encoding for the body part only
2461
        if ('base64' != $this->Encoding and static::hasLineLongerThanMax($this->Body)) {
2462
            $bodyEncoding = 'quoted-printable';
2463
        }
2464
2465
        $altBodyEncoding = $this->Encoding;
2466
        $altBodyCharSet = $this->CharSet;
2467
        //Can we do a 7-bit downgrade?
2468
        if ('8bit' == $altBodyEncoding and !$this->has8bitChars($this->AltBody)) {
2469
            $altBodyEncoding = '7bit';
2470
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2471
            $altBodyCharSet = 'us-ascii';
2472
        }
2473
        //If lines are too long, and we're not already using an encoding that will shorten them,
2474
        //change to quoted-printable transfer encoding for the alt body part only
2475
        if ('base64' != $altBodyEncoding and static::hasLineLongerThanMax($this->AltBody)) {
2476
            $altBodyEncoding = 'quoted-printable';
2477
        }
2478
        //Use this as a preamble in all multipart message types
2479
        $mimepre = 'This is a multi-part message in MIME format.' . static::$LE;
2480
        switch ($this->message_type) {
2481 View Code Duplication
            case 'inline':
2482
                $body .= $mimepre;
2483
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2484
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2485
                $body .= static::$LE;
2486
                $body .= $this->attachAll('inline', $this->boundary[1]);
2487
                break;
2488 View Code Duplication
            case 'attach':
2489
                $body .= $mimepre;
2490
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2491
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2492
                $body .= static::$LE;
2493
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2494
                break;
2495
            case 'inline_attach':
2496
                $body .= $mimepre;
2497
                $body .= $this->textLine('--' . $this->boundary[1]);
2498
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2499
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2500
                $body .= static::$LE;
2501
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
2502
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2503
                $body .= static::$LE;
2504
                $body .= $this->attachAll('inline', $this->boundary[2]);
2505
                $body .= static::$LE;
2506
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2507
                break;
2508
            case 'alt':
2509
                $body .= $mimepre;
2510
                $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2511
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2512
                $body .= static::$LE;
2513
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, 'text/html', $bodyEncoding);
2514
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2515
                $body .= static::$LE;
2516 View Code Duplication
                if (!empty($this->Ical)) {
2517
                    $body .= $this->getBoundary($this->boundary[1], '', 'text/calendar; method=REQUEST', '');
2518
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2519
                    $body .= static::$LE;
2520
                }
2521
                $body .= $this->endBoundary($this->boundary[1]);
2522
                break;
2523
            case 'alt_inline':
2524
                $body .= $mimepre;
2525
                $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2526
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2527
                $body .= static::$LE;
2528
                $body .= $this->textLine('--' . $this->boundary[1]);
2529
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2530
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
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
                $body .= $this->attachAll('inline', $this->boundary[2]);
2536
                $body .= static::$LE;
2537
                $body .= $this->endBoundary($this->boundary[1]);
2538
                break;
2539
            case 'alt_attach':
2540
                $body .= $mimepre;
2541
                $body .= $this->textLine('--' . $this->boundary[1]);
2542
                $body .= $this->headerLine('Content-Type', 'multipart/alternative;');
2543
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2544
                $body .= static::$LE;
2545
                $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2546
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2547
                $body .= static::$LE;
2548
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding);
2549
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2550
                $body .= static::$LE;
2551 View Code Duplication
                if (!empty($this->Ical)) {
2552
                    $body .= $this->getBoundary($this->boundary[2], '', 'text/calendar; method=REQUEST', '');
2553
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2554
                }
2555
                $body .= $this->endBoundary($this->boundary[2]);
2556
                $body .= static::$LE;
2557
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2558
                break;
2559
            case 'alt_inline_attach':
2560
                $body .= $mimepre;
2561
                $body .= $this->textLine('--' . $this->boundary[1]);
2562
                $body .= $this->headerLine('Content-Type', 'multipart/alternative;');
2563
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"');
2564
                $body .= static::$LE;
2565
                $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding);
2566
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2567
                $body .= static::$LE;
2568
                $body .= $this->textLine('--' . $this->boundary[2]);
2569
                $body .= $this->headerLine('Content-Type', 'multipart/related;');
2570
                $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"');
2571
                $body .= static::$LE;
2572
                $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, 'text/html', $bodyEncoding);
2573
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2574
                $body .= static::$LE;
2575
                $body .= $this->attachAll('inline', $this->boundary[3]);
2576
                $body .= static::$LE;
2577
                $body .= $this->endBoundary($this->boundary[2]);
2578
                $body .= static::$LE;
2579
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2580
                break;
2581
            default:
2582
                // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
2583
                //Reset the `Encoding` property in case we changed it for line length reasons
2584
                $this->Encoding = $bodyEncoding;
2585
                $body .= $this->encodeString($this->Body, $this->Encoding);
2586
                break;
2587
        }
2588
2589
        if ($this->isError()) {
2590
            $body = '';
2591
            if ($this->exceptions) {
2592
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
2593
            }
2594
        } elseif ($this->sign_key_file) {
2595
            try {
2596
                if (!defined('PKCS7_TEXT')) {
2597
                    throw new Exception($this->lang('extension_missing') . 'openssl');
2598
                }
2599
                // @TODO would be nice to use php://temp streams here
2600
                $file = tempnam(sys_get_temp_dir(), 'mail');
2601
                if (false === file_put_contents($file, $body)) {
2602
                    throw new Exception($this->lang('signing') . ' Could not write temp file');
2603
                }
2604
                $signed = tempnam(sys_get_temp_dir(), 'signed');
2605
                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
2606
                if (empty($this->sign_extracerts_file)) {
2607
                    $sign = @openssl_pkcs7_sign(
2608
                        $file,
2609
                        $signed,
2610
                        'file://' . realpath($this->sign_cert_file),
2611
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2612
                        []
2613
                    );
2614
                } else {
2615
                    $sign = @openssl_pkcs7_sign(
2616
                        $file,
2617
                        $signed,
2618
                        'file://' . realpath($this->sign_cert_file),
2619
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2620
                        [],
2621
                        PKCS7_DETACHED,
2622
                        $this->sign_extracerts_file
2623
                    );
2624
                }
2625
                @unlink($file);
2626
                if ($sign) {
2627
                    $body = file_get_contents($signed);
2628
                    @unlink($signed);
2629
                    //The message returned by openssl contains both headers and body, so need to split them up
2630
                    $parts = explode("\n\n", $body, 2);
2631
                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
2632
                    $body = $parts[1];
2633
                } else {
2634
                    @unlink($signed);
2635
                    throw new Exception($this->lang('signing') . openssl_error_string());
2636
                }
2637
            } catch (Exception $exc) {
2638
                $body = '';
2639
                if ($this->exceptions) {
2640
                    throw $exc;
2641
                }
2642
            }
2643
        }
2644
2645
        return $body;
2646
    }
2647
2648
    /**
2649
     * Return the start of a message boundary.
2650
     *
2651
     * @param string $boundary
2652
     * @param string $charSet
2653
     * @param string $contentType
2654
     * @param string $encoding
2655
     *
2656
     * @return string
2657
     */
2658
    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
2659
    {
2660
        $result = '';
2661
        if ('' == $charSet) {
2662
            $charSet = $this->CharSet;
2663
        }
2664
        if ('' == $contentType) {
2665
            $contentType = $this->ContentType;
2666
        }
2667
        if ('' == $encoding) {
2668
            $encoding = $this->Encoding;
2669
        }
2670
        $result .= $this->textLine('--' . $boundary);
2671
        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
2672
        $result .= static::$LE;
2673
        // RFC1341 part 5 says 7bit is assumed if not specified
2674
        if ('7bit' != $encoding) {
2675
            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
2676
        }
2677
        $result .= static::$LE;
2678
2679
        return $result;
2680
    }
2681
2682
    /**
2683
     * Return the end of a message boundary.
2684
     *
2685
     * @param string $boundary
2686
     *
2687
     * @return string
2688
     */
2689
    protected function endBoundary($boundary)
2690
    {
2691
        return static::$LE . '--' . $boundary . '--' . static::$LE;
2692
    }
2693
2694
    /**
2695
     * Set the message type.
2696
     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
2697
     */
2698
    protected function setMessageType()
2699
    {
2700
        $type = [];
2701
        if ($this->alternativeExists()) {
2702
            $type[] = 'alt';
2703
        }
2704
        if ($this->inlineImageExists()) {
2705
            $type[] = 'inline';
2706
        }
2707
        if ($this->attachmentExists()) {
2708
            $type[] = 'attach';
2709
        }
2710
        $this->message_type = implode('_', $type);
2711
        if ('' == $this->message_type) {
2712
            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
2713
            $this->message_type = 'plain';
2714
        }
2715
    }
2716
2717
    /**
2718
     * Format a header line.
2719
     *
2720
     * @param string     $name
2721
     * @param string|int $value
2722
     *
2723
     * @return string
2724
     */
2725
    public function headerLine($name, $value)
2726
    {
2727
        return $name . ': ' . $value . static::$LE;
2728
    }
2729
2730
    /**
2731
     * Return a formatted mail line.
2732
     *
2733
     * @param string $value
2734
     *
2735
     * @return string
2736
     */
2737
    public function textLine($value)
2738
    {
2739
        return $value . static::$LE;
2740
    }
2741
2742
    /**
2743
     * Add an attachment from a path on the filesystem.
2744
     * Never use a user-supplied path to a file!
2745
     * Returns false if the file could not be found or read.
2746
     *
2747
     * @param string $path        Path to the attachment
2748
     * @param string $name        Overrides the attachment name
2749
     * @param string $encoding    File encoding (see $Encoding)
2750
     * @param string $type        File extension (MIME) type
2751
     * @param string $disposition Disposition to use
2752
     *
2753
     * @throws Exception
2754
     *
2755
     * @return bool
2756
     */
2757
    public function addAttachment($path, $name = '', $encoding = 'base64', $type = '', $disposition = 'attachment')
2758
    {
2759
        try {
2760
            if (!@is_file($path)) {
2761
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
2762
            }
2763
2764
            // If a MIME type is not specified, try to work it out from the file name
2765
            if ('' == $type) {
2766
                $type = static::filenameToType($path);
2767
            }
2768
2769
            $filename = basename($path);
2770
            if ('' == $name) {
2771
                $name = $filename;
2772
            }
2773
2774
            $this->attachment[] = [
2775
                0 => $path,
2776
                1 => $filename,
2777
                2 => $name,
2778
                3 => $encoding,
2779
                4 => $type,
2780
                5 => false, // isStringAttachment
2781
                6 => $disposition,
2782
                7 => $name,
2783
            ];
2784
        } catch (Exception $exc) {
2785
            $this->setError($exc->getMessage());
2786
            $this->edebug($exc->getMessage());
2787
            if ($this->exceptions) {
2788
                throw $exc;
2789
            }
2790
2791
            return false;
2792
        }
2793
2794
        return true;
2795
    }
2796
2797
    /**
2798
     * Return the array of attachments.
2799
     *
2800
     * @return array
2801
     */
2802
    public function getAttachments()
2803
    {
2804
        return $this->attachment;
2805
    }
2806
2807
    /**
2808
     * Attach all file, string, and binary attachments to the message.
2809
     * Returns an empty string on failure.
2810
     *
2811
     * @param string $disposition_type
2812
     * @param string $boundary
2813
     *
2814
     * @return string
2815
     */
2816
    protected function attachAll($disposition_type, $boundary)
2817
    {
2818
        // Return text of body
2819
        $mime = [];
2820
        $cidUniq = [];
2821
        $incl = [];
2822
2823
        // Add all attachments
2824
        foreach ($this->attachment as $attachment) {
2825
            // Check if it is a valid disposition_filter
2826
            if ($attachment[6] == $disposition_type) {
2827
                // Check for string attachment
2828
                $string = '';
2829
                $path = '';
2830
                $bString = $attachment[5];
2831
                if ($bString) {
2832
                    $string = $attachment[0];
2833
                } else {
2834
                    $path = $attachment[0];
2835
                }
2836
2837
                $inclhash = hash('sha256', serialize($attachment));
2838
                if (in_array($inclhash, $incl)) {
2839
                    continue;
2840
                }
2841
                $incl[] = $inclhash;
2842
                $name = $attachment[2];
2843
                $encoding = $attachment[3];
2844
                $type = $attachment[4];
2845
                $disposition = $attachment[6];
2846
                $cid = $attachment[7];
2847
                if ('inline' == $disposition and array_key_exists($cid, $cidUniq)) {
2848
                    continue;
2849
                }
2850
                $cidUniq[$cid] = true;
2851
2852
                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
2853
                //Only include a filename property if we have one
2854
                if (!empty($name)) {
2855
                    $mime[] = sprintf(
2856
                        'Content-Type: %s; name="%s"%s',
2857
                        $type,
2858
                        $this->encodeHeader($this->secureHeader($name)),
2859
                        static::$LE
2860
                    );
2861
                } else {
2862
                    $mime[] = sprintf(
2863
                        'Content-Type: %s%s',
2864
                        $type,
2865
                        static::$LE
2866
                    );
2867
                }
2868
                // RFC1341 part 5 says 7bit is assumed if not specified
2869
                if ('7bit' != $encoding) {
2870
                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
2871
                }
2872
2873
                if (!empty($cid)) {
2874
                    $mime[] = sprintf('Content-ID: <%s>%s', $cid, static::$LE);
2875
                }
2876
2877
                // If a filename contains any of these chars, it should be quoted,
2878
                // but not otherwise: RFC2183 & RFC2045 5.1
2879
                // Fixes a warning in IETF's msglint MIME checker
2880
                // Allow for bypassing the Content-Disposition header totally
2881
                if (!(empty($disposition))) {
2882
                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
2883
                    if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) {
2884
                        $mime[] = sprintf(
2885
                            'Content-Disposition: %s; filename="%s"%s',
2886
                            $disposition,
2887
                            $encoded_name,
2888
                            static::$LE . static::$LE
2889
                        );
2890
                    } else {
2891
                        if (!empty($encoded_name)) {
2892
                            $mime[] = sprintf(
2893
                                'Content-Disposition: %s; filename=%s%s',
2894
                                $disposition,
2895
                                $encoded_name,
2896
                                static::$LE . static::$LE
2897
                            );
2898
                        } else {
2899
                            $mime[] = sprintf(
2900
                                'Content-Disposition: %s%s',
2901
                                $disposition,
2902
                                static::$LE . static::$LE
2903
                            );
2904
                        }
2905
                    }
2906
                } else {
2907
                    $mime[] = static::$LE;
2908
                }
2909
2910
                // Encode as string attachment
2911
                if ($bString) {
2912
                    $mime[] = $this->encodeString($string, $encoding);
2913
                } else {
2914
                    $mime[] = $this->encodeFile($path, $encoding);
2915
                }
2916
                if ($this->isError()) {
2917
                    return '';
2918
                }
2919
                $mime[] = static::$LE;
2920
            }
2921
        }
2922
2923
        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
2924
2925
        return implode('', $mime);
2926
    }
2927
2928
    /**
2929
     * Encode a file attachment in requested format.
2930
     * Returns an empty string on failure.
2931
     *
2932
     * @param string $path     The full path to the file
2933
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
2934
     *
2935
     * @throws Exception
2936
     *
2937
     * @return string
2938
     */
2939
    protected function encodeFile($path, $encoding = 'base64')
2940
    {
2941
        try {
2942
            if (!file_exists($path)) {
2943
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
2944
            }
2945
            $file_buffer = file_get_contents($path);
2946
            if (false === $file_buffer) {
2947
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
2948
            }
2949
            $file_buffer = $this->encodeString($file_buffer, $encoding);
2950
2951
            return $file_buffer;
2952
        } catch (Exception $exc) {
2953
            $this->setError($exc->getMessage());
2954
2955
            return '';
2956
        }
2957
    }
2958
2959
    /**
2960
     * Encode a string in requested format.
2961
     * Returns an empty string on failure.
2962
     *
2963
     * @param string $str      The text to encode
2964
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable
2965
     *
2966
     * @return string
2967
     */
2968
    public function encodeString($str, $encoding = 'base64')
2969
    {
2970
        $encoded = '';
2971
        switch (strtolower($encoding)) {
2972
            case 'base64':
2973
                $encoded = chunk_split(
2974
                    base64_encode($str),
2975
                    static::STD_LINE_LENGTH,
2976
                    static::$LE
2977
                );
2978
                break;
2979
            case '7bit':
2980
            case '8bit':
2981
                $encoded = static::normalizeBreaks($str);
2982
                // Make sure it ends with a line break
2983
                if (substr($encoded, -(strlen(static::$LE))) != static::$LE) {
2984
                    $encoded .= static::$LE;
2985
                }
2986
                break;
2987
            case 'binary':
2988
                $encoded = $str;
2989
                break;
2990
            case 'quoted-printable':
2991
                $encoded = $this->encodeQP($str);
2992
                break;
2993
            default:
2994
                $this->setError($this->lang('encoding') . $encoding);
2995
                break;
2996
        }
2997
2998
        return $encoded;
2999
    }
3000
3001
    /**
3002
     * Encode a header value (not including its label) optimally.
3003
     * Picks shortest of Q, B, or none. Result includes folding if needed.
3004
     * See RFC822 definitions for phrase, comment and text positions.
3005
     *
3006
     * @param string $str      The header value to encode
3007
     * @param string $position What context the string will be used in
3008
     *
3009
     * @return string
3010
     */
3011
    public function encodeHeader($str, $position = 'text')
3012
    {
3013
        $matchcount = 0;
3014
        switch (strtolower($position)) {
3015
            case 'phrase':
3016
                if (!preg_match('/[\200-\377]/', $str)) {
3017
                    // Can't use addslashes as we don't know the value of magic_quotes_sybase
3018
                    $encoded = addcslashes($str, "\0..\37\177\\\"");
3019
                    if (($str == $encoded) and !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
3020
                        return $encoded;
3021
                    }
3022
3023
                    return "\"$encoded\"";
3024
                }
3025
                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
3026
                break;
3027
            /* @noinspection PhpMissingBreakStatementInspection */
3028
            case 'comment':
3029
                $matchcount = preg_match_all('/[()"]/', $str, $matches);
3030
            //fallthrough
3031
            case 'text':
3032
            default:
3033
                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
3034
                break;
3035
        }
3036
3037
        //RFCs specify a maximum line length of 78 chars, however mail() will sometimes
3038
        //corrupt messages with headers longer than 65 chars. See #818
3039
        $lengthsub = 'mail' == $this->Mailer ? 13 : 0;
3040
        $maxlen = static::STD_LINE_LENGTH - $lengthsub;
3041
        // Try to select the encoding which should produce the shortest output
3042
        if ($matchcount > strlen($str) / 3) {
3043
            // More than a third of the content will need encoding, so B encoding will be most efficient
3044
            $encoding = 'B';
3045
            //This calculation is:
3046
            // max line length
3047
            // - shorten to avoid mail() corruption
3048
            // - Q/B encoding char overhead ("` =?<charset>?[QB]?<content>?=`")
3049
            // - charset name length
3050
            $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet);
3051
            if ($this->hasMultiBytes($str)) {
3052
                // Use a custom function which correctly encodes and wraps long
3053
                // multibyte strings without breaking lines within a character
3054
                $encoded = $this->base64EncodeWrapMB($str, "\n");
3055
            } else {
3056
                $encoded = base64_encode($str);
3057
                $maxlen -= $maxlen % 4;
3058
                $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
3059
            }
3060
            $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
3061
        } elseif ($matchcount > 0) {
3062
            //1 or more chars need encoding, use Q-encode
3063
            $encoding = 'Q';
3064
            //Recalc max line length for Q encoding - see comments on B encode
3065
            $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet);
3066
            $encoded = $this->encodeQ($str, $position);
3067
            $encoded = $this->wrapText($encoded, $maxlen, true);
3068
            $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
3069
            $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
3070
        } elseif (strlen($str) > $maxlen) {
3071
            //No chars need encoding, but line is too long, so fold it
3072
            $encoded = trim($this->wrapText($str, $maxlen, false));
3073
            if ($str == $encoded) {
3074
                //Wrapping nicely didn't work, wrap hard instead
3075
                $encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE));
3076
            }
3077
            $encoded = str_replace(static::$LE, "\n", trim($encoded));
3078
            $encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded);
3079
        } else {
3080
            //No reformatting needed
3081
            return $str;
3082
        }
3083
3084
        return trim(static::normalizeBreaks($encoded));
3085
    }
3086
3087
    /**
3088
     * Check if a string contains multi-byte characters.
3089
     *
3090
     * @param string $str multi-byte text to wrap encode
3091
     *
3092
     * @return bool
3093
     */
3094
    public function hasMultiBytes($str)
3095
    {
3096
        if (function_exists('mb_strlen')) {
3097
            return strlen($str) > mb_strlen($str, $this->CharSet);
3098
        }
3099
3100
        // Assume no multibytes (we can't handle without mbstring functions anyway)
3101
        return false;
3102
    }
3103
3104
    /**
3105
     * Does a string contain any 8-bit chars (in any charset)?
3106
     *
3107
     * @param string $text
3108
     *
3109
     * @return bool
3110
     */
3111
    public function has8bitChars($text)
3112
    {
3113
        return (bool) preg_match('/[\x80-\xFF]/', $text);
3114
    }
3115
3116
    /**
3117
     * Encode and wrap long multibyte strings for mail headers
3118
     * without breaking lines within a character.
3119
     * Adapted from a function by paravoid.
3120
     *
3121
     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
3122
     *
3123
     * @param string $str       multi-byte text to wrap encode
3124
     * @param string $linebreak string to use as linefeed/end-of-line
0 ignored issues
show
Documentation introduced by
Should the type for parameter $linebreak not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
3125
     *
3126
     * @return string
3127
     */
3128
    public function base64EncodeWrapMB($str, $linebreak = null)
3129
    {
3130
        $start = '=?' . $this->CharSet . '?B?';
3131
        $end = '?=';
3132
        $encoded = '';
3133
        if (null === $linebreak) {
3134
            $linebreak = static::$LE;
3135
        }
3136
3137
        $mb_length = mb_strlen($str, $this->CharSet);
3138
        // Each line must have length <= 75, including $start and $end
3139
        $length = 75 - strlen($start) - strlen($end);
3140
        // Average multi-byte ratio
3141
        $ratio = $mb_length / strlen($str);
3142
        // Base64 has a 4:3 ratio
3143
        $avgLength = floor($length * $ratio * .75);
3144
3145
        for ($i = 0; $i < $mb_length; $i += $offset) {
3146
            $lookBack = 0;
3147
            do {
3148
                $offset = $avgLength - $lookBack;
3149
                $chunk = mb_substr($str, $i, $offset, $this->CharSet);
3150
                $chunk = base64_encode($chunk);
3151
                ++$lookBack;
3152
            } while (strlen($chunk) > $length);
3153
            $encoded .= $chunk . $linebreak;
3154
        }
3155
3156
        // Chomp the last linefeed
3157
        return substr($encoded, 0, -strlen($linebreak));
3158
    }
3159
3160
    /**
3161
     * Encode a string in quoted-printable format.
3162
     * According to RFC2045 section 6.7.
3163
     *
3164
     * @param string $string The text to encode
3165
     *
3166
     * @return string
3167
     */
3168
    public function encodeQP($string)
3169
    {
3170
        return static::normalizeBreaks(quoted_printable_encode($string));
3171
    }
3172
3173
    /**
3174
     * Encode a string using Q encoding.
3175
     *
3176
     * @see http://tools.ietf.org/html/rfc2047#section-4.2
3177
     *
3178
     * @param string $str      the text to encode
3179
     * @param string $position Where the text is going to be used, see the RFC for what that means
3180
     *
3181
     * @return string
3182
     */
3183
    public function encodeQ($str, $position = 'text')
3184
    {
3185
        // There should not be any EOL in the string
3186
        $pattern = '';
3187
        $encoded = str_replace(["\r", "\n"], '', $str);
3188
        switch (strtolower($position)) {
3189
            case 'phrase':
3190
                // RFC 2047 section 5.3
3191
                $pattern = '^A-Za-z0-9!*+\/ -';
3192
                break;
3193
            /*
3194
             * RFC 2047 section 5.2.
3195
             * Build $pattern without including delimiters and []
3196
             */
3197
            /* @noinspection PhpMissingBreakStatementInspection */
3198
            case 'comment':
3199
                $pattern = '\(\)"';
3200
            /* Intentional fall through */
3201
            case 'text':
3202
            default:
3203
                // RFC 2047 section 5.1
3204
                // Replace every high ascii, control, =, ? and _ characters
3205
                /** @noinspection SuspiciousAssignmentsInspection */
3206
                $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
3207
                break;
3208
        }
3209
        $matches = [];
3210
        if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
3211
            // If the string contains an '=', make sure it's the first thing we replace
3212
            // so as to avoid double-encoding
3213
            $eqkey = array_search('=', $matches[0]);
3214
            if (false !== $eqkey) {
3215
                unset($matches[0][$eqkey]);
3216
                array_unshift($matches[0], '=');
3217
            }
3218
            foreach (array_unique($matches[0]) as $char) {
3219
                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
3220
            }
3221
        }
3222
        // Replace spaces with _ (more readable than =20)
3223
        // RFC 2047 section 4.2(2)
3224
        return str_replace(' ', '_', $encoded);
3225
    }
3226
3227
    /**
3228
     * Add a string or binary attachment (non-filesystem).
3229
     * This method can be used to attach ascii or binary data,
3230
     * such as a BLOB record from a database.
3231
     *
3232
     * @param string $string      String attachment data
3233
     * @param string $filename    Name of the attachment
3234
     * @param string $encoding    File encoding (see $Encoding)
3235
     * @param string $type        File extension (MIME) type
3236
     * @param string $disposition Disposition to use
3237
     */
3238
    public function addStringAttachment(
3239
        $string,
3240
        $filename,
3241
        $encoding = 'base64',
3242
        $type = '',
3243
        $disposition = 'attachment'
3244
    ) {
3245
        // If a MIME type is not specified, try to work it out from the file name
3246
        if ('' == $type) {
3247
            $type = static::filenameToType($filename);
3248
        }
3249
        // Append to $attachment array
3250
        $this->attachment[] = [
3251
            0 => $string,
3252
            1 => $filename,
3253
            2 => basename($filename),
3254
            3 => $encoding,
3255
            4 => $type,
3256
            5 => true, // isStringAttachment
3257
            6 => $disposition,
3258
            7 => 0,
3259
        ];
3260
    }
3261
3262
    /**
3263
     * Add an embedded (inline) attachment from a file.
3264
     * This can include images, sounds, and just about any other document type.
3265
     * These differ from 'regular' attachments in that they are intended to be
3266
     * displayed inline with the message, not just attached for download.
3267
     * This is used in HTML messages that embed the images
3268
     * the HTML refers to using the $cid value.
3269
     * Never use a user-supplied path to a file!
3270
     *
3271
     * @param string $path        Path to the attachment
3272
     * @param string $cid         Content ID of the attachment; Use this to reference
3273
     *                            the content when using an embedded image in HTML
3274
     * @param string $name        Overrides the attachment name
3275
     * @param string $encoding    File encoding (see $Encoding)
3276
     * @param string $type        File MIME type
3277
     * @param string $disposition Disposition to use
3278
     *
3279
     * @return bool True on successfully adding an attachment
3280
     */
3281
    public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = '', $disposition = 'inline')
3282
    {
3283
        if (!@is_file($path)) {
3284
            $this->setError($this->lang('file_access') . $path);
3285
3286
            return false;
3287
        }
3288
3289
        // If a MIME type is not specified, try to work it out from the file name
3290
        if ('' == $type) {
3291
            $type = static::filenameToType($path);
3292
        }
3293
3294
        $filename = basename($path);
3295
        if ('' == $name) {
3296
            $name = $filename;
3297
        }
3298
3299
        // Append to $attachment array
3300
        $this->attachment[] = [
3301
            0 => $path,
3302
            1 => $filename,
3303
            2 => $name,
3304
            3 => $encoding,
3305
            4 => $type,
3306
            5 => false, // isStringAttachment
3307
            6 => $disposition,
3308
            7 => $cid,
3309
        ];
3310
3311
        return true;
3312
    }
3313
3314
    /**
3315
     * Add an embedded stringified attachment.
3316
     * This can include images, sounds, and just about any other document type.
3317
     * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.
3318
     *
3319
     * @param string $string      The attachment binary data
3320
     * @param string $cid         Content ID of the attachment; Use this to reference
3321
     *                            the content when using an embedded image in HTML
3322
     * @param string $name        A filename for the attachment. If this contains an extension,
3323
     *                            PHPMailer will attempt to set a MIME type for the attachment.
3324
     *                            For example 'file.jpg' would get an 'image/jpeg' MIME type.
3325
     * @param string $encoding    File encoding (see $Encoding), defaults to 'base64'
3326
     * @param string $type        MIME type - will be used in preference to any automatically derived type
3327
     * @param string $disposition Disposition to use
3328
     *
3329
     * @return bool True on successfully adding an attachment
3330
     */
3331
    public function addStringEmbeddedImage(
3332
        $string,
3333
        $cid,
3334
        $name = '',
3335
        $encoding = 'base64',
3336
        $type = '',
3337
        $disposition = 'inline'
3338
    ) {
3339
        // If a MIME type is not specified, try to work it out from the name
3340
        if ('' == $type and !empty($name)) {
3341
            $type = static::filenameToType($name);
3342
        }
3343
3344
        // Append to $attachment array
3345
        $this->attachment[] = [
3346
            0 => $string,
3347
            1 => $name,
3348
            2 => $name,
3349
            3 => $encoding,
3350
            4 => $type,
3351
            5 => true, // isStringAttachment
3352
            6 => $disposition,
3353
            7 => $cid,
3354
        ];
3355
3356
        return true;
3357
    }
3358
3359
    /**
3360
     * Check if an embedded attachment is present with this cid.
3361
     *
3362
     * @param string $cid
3363
     *
3364
     * @return bool
3365
     */
3366 View Code Duplication
    protected function cidExists($cid)
3367
    {
3368
        foreach ($this->attachment as $attachment) {
3369
            if ('inline' == $attachment[6] and $cid == $attachment[7]) {
3370
                return true;
3371
            }
3372
        }
3373
3374
        return false;
3375
    }
3376
3377
    /**
3378
     * Check if an inline attachment is present.
3379
     *
3380
     * @return bool
3381
     */
3382 View Code Duplication
    public function inlineImageExists()
3383
    {
3384
        foreach ($this->attachment as $attachment) {
3385
            if ('inline' == $attachment[6]) {
3386
                return true;
3387
            }
3388
        }
3389
3390
        return false;
3391
    }
3392
3393
    /**
3394
     * Check if an attachment (non-inline) is present.
3395
     *
3396
     * @return bool
3397
     */
3398
    public function attachmentExists()
3399
    {
3400
        foreach ($this->attachment as $attachment) {
3401
            if ('attachment' == $attachment[6]) {
3402
                return true;
3403
            }
3404
        }
3405
3406
        return false;
3407
    }
3408
3409
    /**
3410
     * Check if this message has an alternative body set.
3411
     *
3412
     * @return bool
3413
     */
3414
    public function alternativeExists()
3415
    {
3416
        return !empty($this->AltBody);
3417
    }
3418
3419
    /**
3420
     * Clear queued addresses of given kind.
3421
     *
3422
     * @param string $kind 'to', 'cc', or 'bcc'
3423
     */
3424
    public function clearQueuedAddresses($kind)
3425
    {
3426
        $this->RecipientsQueue = array_filter(
3427
            $this->RecipientsQueue,
3428
            function ($params) use ($kind) {
3429
                return $params[0] != $kind;
3430
            }
3431
        );
3432
    }
3433
3434
    /**
3435
     * Clear all To recipients.
3436
     */
3437
    public function clearAddresses()
3438
    {
3439
        foreach ($this->to as $to) {
3440
            unset($this->all_recipients[strtolower($to[0])]);
3441
        }
3442
        $this->to = [];
3443
        $this->clearQueuedAddresses('to');
3444
    }
3445
3446
    /**
3447
     * Clear all CC recipients.
3448
     */
3449 View Code Duplication
    public function clearCCs()
3450
    {
3451
        foreach ($this->cc as $cc) {
3452
            unset($this->all_recipients[strtolower($cc[0])]);
3453
        }
3454
        $this->cc = [];
3455
        $this->clearQueuedAddresses('cc');
3456
    }
3457
3458
    /**
3459
     * Clear all BCC recipients.
3460
     */
3461 View Code Duplication
    public function clearBCCs()
3462
    {
3463
        foreach ($this->bcc as $bcc) {
3464
            unset($this->all_recipients[strtolower($bcc[0])]);
3465
        }
3466
        $this->bcc = [];
3467
        $this->clearQueuedAddresses('bcc');
3468
    }
3469
3470
    /**
3471
     * Clear all ReplyTo recipients.
3472
     */
3473
    public function clearReplyTos()
3474
    {
3475
        $this->ReplyTo = [];
3476
        $this->ReplyToQueue = [];
3477
    }
3478
3479
    /**
3480
     * Clear all recipient types.
3481
     */
3482
    public function clearAllRecipients()
3483
    {
3484
        $this->to = [];
3485
        $this->cc = [];
3486
        $this->bcc = [];
3487
        $this->all_recipients = [];
3488
        $this->RecipientsQueue = [];
3489
    }
3490
3491
    /**
3492
     * Clear all filesystem, string, and binary attachments.
3493
     */
3494
    public function clearAttachments()
3495
    {
3496
        $this->attachment = [];
3497
    }
3498
3499
    /**
3500
     * Clear all custom headers.
3501
     */
3502
    public function clearCustomHeaders()
3503
    {
3504
        $this->CustomHeader = [];
3505
    }
3506
3507
    /**
3508
     * Add an error message to the error container.
3509
     *
3510
     * @param string $msg
3511
     */
3512
    protected function setError($msg)
3513
    {
3514
        ++$this->error_count;
3515
        if ('smtp' == $this->Mailer and null !== $this->smtp) {
3516
            $lasterror = $this->smtp->getError();
3517
            if (!empty($lasterror['error'])) {
3518
                $msg .= $this->lang('smtp_error') . $lasterror['error'];
3519
                if (!empty($lasterror['detail'])) {
3520
                    $msg .= ' Detail: ' . $lasterror['detail'];
3521
                }
3522
                if (!empty($lasterror['smtp_code'])) {
3523
                    $msg .= ' SMTP code: ' . $lasterror['smtp_code'];
3524
                }
3525
                if (!empty($lasterror['smtp_code_ex'])) {
3526
                    $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex'];
3527
                }
3528
            }
3529
        }
3530
        $this->ErrorInfo = $msg;
3531
    }
3532
3533
    /**
3534
     * Return an RFC 822 formatted date.
3535
     *
3536
     * @return string
3537
     */
3538
    public static function rfcDate()
3539
    {
3540
        // Set the time zone to whatever the default is to avoid 500 errors
3541
        // Will default to UTC if it's not set properly in php.ini
3542
        date_default_timezone_set(@date_default_timezone_get());
3543
3544
        return date('D, j M Y H:i:s O');
3545
    }
3546
3547
    /**
3548
     * Get the server hostname.
3549
     * Returns 'localhost.localdomain' if unknown.
3550
     *
3551
     * @return string
3552
     */
3553
    protected function serverHostname()
3554
    {
3555
        $result = '';
3556
        if (!empty($this->Hostname)) {
3557
            $result = $this->Hostname;
3558
        } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER)) {
3559
            $result = $_SERVER['SERVER_NAME'];
3560
        } elseif (function_exists('gethostname') and gethostname() !== false) {
3561
            $result = gethostname();
3562
        } elseif (php_uname('n') !== false) {
3563
            $result = php_uname('n');
3564
        }
3565
        if (!static::isValidHost($result)) {
3566
            return 'localhost.localdomain';
3567
        }
3568
3569
        return $result;
3570
    }
3571
3572
    /**
3573
     * Validate whether a string contains a valid value to use as a hostname or IP address.
3574
     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
3575
     *
3576
     * @param string $host The host name or IP address to check
3577
     *
3578
     * @return bool
3579
     */
3580
    public static function isValidHost($host)
3581
    {
3582
        //Simple syntax limits
3583
        if (empty($host)
3584
            or !is_string($host)
3585
            or strlen($host) > 256
3586
        ) {
3587
            return false;
3588
        }
3589
        //Looks like a bracketed IPv6 address
3590
        if (trim($host, '[]') != $host) {
3591
            return (bool) filter_var(trim($host, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
3592
        }
3593
        //If removing all the dots results in a numeric string, it must be an IPv4 address.
3594
        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
3595
        if (is_numeric(str_replace('.', '', $host))) {
3596
            //Is it a valid IPv4 address?
3597
            return (bool) filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
3598
        }
3599
        if (filter_var('http://' . $host, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return (bool) filter_var...ER_FLAG_HOST_REQUIRED);.
Loading history...
3600
            //Is it a syntactically valid hostname?
3601
            return true;
3602
        }
3603
3604
        return false;
3605
    }
3606
3607
    /**
3608
     * Get an error message in the current language.
3609
     *
3610
     * @param string $key
3611
     *
3612
     * @return string
3613
     */
3614
    protected function lang($key)
3615
    {
3616
        if (count($this->language) < 1) {
3617
            $this->setLanguage('en'); // set the default language
3618
        }
3619
3620
        if (array_key_exists($key, $this->language)) {
3621
            if ('smtp_connect_failed' == $key) {
3622
                //Include a link to troubleshooting docs on SMTP connection failure
3623
                //this is by far the biggest cause of support questions
3624
                //but it's usually not PHPMailer's fault.
3625
                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
3626
            }
3627
3628
            return $this->language[$key];
3629
        }
3630
3631
        //Return the key as a fallback
3632
        return $key;
3633
    }
3634
3635
    /**
3636
     * Check if an error occurred.
3637
     *
3638
     * @return bool True if an error did occur
3639
     */
3640
    public function isError()
3641
    {
3642
        return $this->error_count > 0;
3643
    }
3644
3645
    /**
3646
     * Add a custom header.
3647
     * $name value can be overloaded to contain
3648
     * both header name and value (name:value).
3649
     *
3650
     * @param string      $name  Custom header name
3651
     * @param string|null $value Header value
3652
     */
3653
    public function addCustomHeader($name, $value = null)
3654
    {
3655
        if (null === $value) {
3656
            // Value passed in as name:value
3657
            $this->CustomHeader[] = explode(':', $name, 2);
3658
        } else {
3659
            $this->CustomHeader[] = [$name, $value];
3660
        }
3661
    }
3662
3663
    /**
3664
     * Returns all custom headers.
3665
     *
3666
     * @return array
3667
     */
3668
    public function getCustomHeaders()
3669
    {
3670
        return $this->CustomHeader;
3671
    }
3672
3673
    /**
3674
     * Create a message body from an HTML string.
3675
     * Automatically inlines images and creates a plain-text version by converting the HTML,
3676
     * overwriting any existing values in Body and AltBody.
3677
     * Do not source $message content from user input!
3678
     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
3679
     * will look for an image file in $basedir/images/a.png and convert it to inline.
3680
     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
3681
     * Converts data-uri images into embedded attachments.
3682
     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
3683
     *
3684
     * @param string        $message  HTML message string
3685
     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
3686
     * @param bool|callable $advanced Whether to use the internal HTML to text converter
3687
     *                                or your own custom converter @see PHPMailer::html2text()
3688
     *
3689
     * @return string $message The transformed message Body
3690
     */
3691
    public function msgHTML($message, $basedir = '', $advanced = false)
3692
    {
3693
        preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images);
3694
        if (array_key_exists(2, $images)) {
3695 View Code Duplication
            if (strlen($basedir) > 1 && '/' != substr($basedir, -1)) {
3696
                // Ensure $basedir has a trailing /
3697
                $basedir .= '/';
3698
            }
3699
            foreach ($images[2] as $imgindex => $url) {
3700
                // Convert data URIs into embedded images
3701
                //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
3702
                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
3703
                    if (count($match) == 4 and 'base64' == $match[2]) {
3704
                        $data = base64_decode($match[3]);
3705
                    } elseif ('' == $match[2]) {
3706
                        $data = rawurldecode($match[3]);
3707
                    } else {
3708
                        //Not recognised so leave it alone
3709
                        continue;
3710
                    }
3711
                    //Hash the decoded data, not the URL so that the same data-URI image used in multiple places
3712
                    //will only be embedded once, even if it used a different encoding
3713
                    $cid = hash('sha256', $data) . '@phpmailer.0'; // RFC2392 S 2
3714
3715
                    if (!$this->cidExists($cid)) {
3716
                        $this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, 'base64', $match[1]);
3717
                    }
3718
                    $message = str_replace(
3719
                        $images[0][$imgindex],
3720
                        $images[1][$imgindex] . '="cid:' . $cid . '"',
3721
                        $message
3722
                    );
3723
                    continue;
3724
                }
3725
                if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
3726
                    !empty($basedir)
3727
                    // Ignore URLs containing parent dir traversal (..)
3728
                    and (strpos($url, '..') === false)
3729
                    // Do not change urls that are already inline images
3730
                    and 0 !== strpos($url, 'cid:')
3731
                    // Do not change absolute URLs, including anonymous protocol
3732
                    and !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
3733
                ) {
3734
                    $filename = basename($url);
3735
                    $directory = dirname($url);
3736
                    if ('.' == $directory) {
3737
                        $directory = '';
3738
                    }
3739
                    $cid = hash('sha256', $url) . '@phpmailer.0'; // RFC2392 S 2
3740 View Code Duplication
                    if (strlen($basedir) > 1 and '/' != substr($basedir, -1)) {
3741
                        $basedir .= '/';
3742
                    }
3743
                    if (strlen($directory) > 1 and '/' != substr($directory, -1)) {
3744
                        $directory .= '/';
3745
                    }
3746
                    if ($this->addEmbeddedImage(
3747
                        $basedir . $directory . $filename,
3748
                        $cid,
3749
                        $filename,
3750
                        'base64',
3751
                        static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
3752
                    )
3753
                    ) {
3754
                        $message = preg_replace(
3755
                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
3756
                            $images[1][$imgindex] . '="cid:' . $cid . '"',
3757
                            $message
3758
                        );
3759
                    }
3760
                }
3761
            }
3762
        }
3763
        $this->isHTML(true);
3764
        // Convert all message body line breaks to LE, makes quoted-printable encoding work much better
3765
        $this->Body = static::normalizeBreaks($message);
3766
        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
3767
        if (!$this->alternativeExists()) {
3768
            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
3769
                . static::$LE;
3770
        }
3771
3772
        return $this->Body;
3773
    }
3774
3775
    /**
3776
     * Convert an HTML string into plain text.
3777
     * This is used by msgHTML().
3778
     * Note - older versions of this function used a bundled advanced converter
3779
     * which was removed for license reasons in #232.
3780
     * Example usage:
3781
     *
3782
     * ```php
3783
     * // Use default conversion
3784
     * $plain = $mail->html2text($html);
3785
     * // Use your own custom converter
3786
     * $plain = $mail->html2text($html, function($html) {
3787
     *     $converter = new MyHtml2text($html);
3788
     *     return $converter->get_text();
3789
     * });
3790
     * ```
3791
     *
3792
     * @param string        $html     The HTML text to convert
3793
     * @param bool|callable $advanced Any boolean value to use the internal converter,
3794
     *                                or provide your own callable for custom conversion
3795
     *
3796
     * @return string
3797
     */
3798
    public function html2text($html, $advanced = false)
3799
    {
3800
        if (is_callable($advanced)) {
3801
            return call_user_func($advanced, $html);
3802
        }
3803
3804
        return html_entity_decode(
3805
            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
3806
            ENT_QUOTES,
3807
            $this->CharSet
3808
        );
3809
    }
3810
3811
    /**
3812
     * Get the MIME type for a file extension.
3813
     *
3814
     * @param string $ext File extension
3815
     *
3816
     * @return string MIME type of file
3817
     */
3818
    public static function _mime_types($ext = '')
3819
    {
3820
        $mimes = [
3821
            'xl' => 'application/excel',
3822
            'js' => 'application/javascript',
3823
            'hqx' => 'application/mac-binhex40',
3824
            'cpt' => 'application/mac-compactpro',
3825
            'bin' => 'application/macbinary',
3826
            'doc' => 'application/msword',
3827
            'word' => 'application/msword',
3828
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
3829
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
3830
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
3831
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
3832
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
3833
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
3834
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
3835
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
3836
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
3837
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
3838
            'class' => 'application/octet-stream',
3839
            'dll' => 'application/octet-stream',
3840
            'dms' => 'application/octet-stream',
3841
            'exe' => 'application/octet-stream',
3842
            'lha' => 'application/octet-stream',
3843
            'lzh' => 'application/octet-stream',
3844
            'psd' => 'application/octet-stream',
3845
            'sea' => 'application/octet-stream',
3846
            'so' => 'application/octet-stream',
3847
            'oda' => 'application/oda',
3848
            'pdf' => 'application/pdf',
3849
            'ai' => 'application/postscript',
3850
            'eps' => 'application/postscript',
3851
            'ps' => 'application/postscript',
3852
            'smi' => 'application/smil',
3853
            'smil' => 'application/smil',
3854
            'mif' => 'application/vnd.mif',
3855
            'xls' => 'application/vnd.ms-excel',
3856
            'ppt' => 'application/vnd.ms-powerpoint',
3857
            'wbxml' => 'application/vnd.wap.wbxml',
3858
            'wmlc' => 'application/vnd.wap.wmlc',
3859
            'dcr' => 'application/x-director',
3860
            'dir' => 'application/x-director',
3861
            'dxr' => 'application/x-director',
3862
            'dvi' => 'application/x-dvi',
3863
            'gtar' => 'application/x-gtar',
3864
            'php3' => 'application/x-httpd-php',
3865
            'php4' => 'application/x-httpd-php',
3866
            'php' => 'application/x-httpd-php',
3867
            'phtml' => 'application/x-httpd-php',
3868
            'phps' => 'application/x-httpd-php-source',
3869
            'swf' => 'application/x-shockwave-flash',
3870
            'sit' => 'application/x-stuffit',
3871
            'tar' => 'application/x-tar',
3872
            'tgz' => 'application/x-tar',
3873
            'xht' => 'application/xhtml+xml',
3874
            'xhtml' => 'application/xhtml+xml',
3875
            'zip' => 'application/zip',
3876
            'mid' => 'audio/midi',
3877
            'midi' => 'audio/midi',
3878
            'mp2' => 'audio/mpeg',
3879
            'mp3' => 'audio/mpeg',
3880
            'm4a' => 'audio/mp4',
3881
            'mpga' => 'audio/mpeg',
3882
            'aif' => 'audio/x-aiff',
3883
            'aifc' => 'audio/x-aiff',
3884
            'aiff' => 'audio/x-aiff',
3885
            'ram' => 'audio/x-pn-realaudio',
3886
            'rm' => 'audio/x-pn-realaudio',
3887
            'rpm' => 'audio/x-pn-realaudio-plugin',
3888
            'ra' => 'audio/x-realaudio',
3889
            'wav' => 'audio/x-wav',
3890
            'mka' => 'audio/x-matroska',
3891
            'bmp' => 'image/bmp',
3892
            'gif' => 'image/gif',
3893
            'jpeg' => 'image/jpeg',
3894
            'jpe' => 'image/jpeg',
3895
            'jpg' => 'image/jpeg',
3896
            'png' => 'image/png',
3897
            'tiff' => 'image/tiff',
3898
            'tif' => 'image/tiff',
3899
            'webp' => 'image/webp',
3900
            'heif' => 'image/heif',
3901
            'heifs' => 'image/heif-sequence',
3902
            'heic' => 'image/heic',
3903
            'heics' => 'image/heic-sequence',
3904
            'eml' => 'message/rfc822',
3905
            'css' => 'text/css',
3906
            'html' => 'text/html',
3907
            'htm' => 'text/html',
3908
            'shtml' => 'text/html',
3909
            'log' => 'text/plain',
3910
            'text' => 'text/plain',
3911
            'txt' => 'text/plain',
3912
            'rtx' => 'text/richtext',
3913
            'rtf' => 'text/rtf',
3914
            'vcf' => 'text/vcard',
3915
            'vcard' => 'text/vcard',
3916
            'ics' => 'text/calendar',
3917
            'xml' => 'text/xml',
3918
            'xsl' => 'text/xml',
3919
            'wmv' => 'video/x-ms-wmv',
3920
            'mpeg' => 'video/mpeg',
3921
            'mpe' => 'video/mpeg',
3922
            'mpg' => 'video/mpeg',
3923
            'mp4' => 'video/mp4',
3924
            'm4v' => 'video/mp4',
3925
            'mov' => 'video/quicktime',
3926
            'qt' => 'video/quicktime',
3927
            'rv' => 'video/vnd.rn-realvideo',
3928
            'avi' => 'video/x-msvideo',
3929
            'movie' => 'video/x-sgi-movie',
3930
            'webm' => 'video/webm',
3931
            'mkv' => 'video/x-matroska',
3932
        ];
3933
        $ext = strtolower($ext);
3934
        if (array_key_exists($ext, $mimes)) {
3935
            return $mimes[$ext];
3936
        }
3937
3938
        return 'application/octet-stream';
3939
    }
3940
3941
    /**
3942
     * Map a file name to a MIME type.
3943
     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
3944
     *
3945
     * @param string $filename A file name or full path, does not need to exist as a file
3946
     *
3947
     * @return string
3948
     */
3949
    public static function filenameToType($filename)
3950
    {
3951
        // In case the path is a URL, strip any query string before getting extension
3952
        $qpos = strpos($filename, '?');
3953
        if (false !== $qpos) {
3954
            $filename = substr($filename, 0, $qpos);
3955
        }
3956
        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
3957
3958
        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 3956 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...
3959
    }
3960
3961
    /**
3962
     * Multi-byte-safe pathinfo replacement.
3963
     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
3964
     *
3965
     * @see    http://www.php.net/manual/en/function.pathinfo.php#107461
3966
     *
3967
     * @param string     $path    A filename or path, does not need to exist as a file
3968
     * @param int|string $options Either a PATHINFO_* constant,
0 ignored issues
show
Documentation introduced by
Should the type for parameter $options not be integer|string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
3969
     *                            or a string name to return only the specified piece
3970
     *
3971
     * @return string|array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string|array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
3972
     */
3973
    public static function mb_pathinfo($path, $options = null)
3974
    {
3975
        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
3976
        $pathinfo = [];
3977
        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$#im', $path, $pathinfo)) {
3978
            if (array_key_exists(1, $pathinfo)) {
3979
                $ret['dirname'] = $pathinfo[1];
3980
            }
3981
            if (array_key_exists(2, $pathinfo)) {
3982
                $ret['basename'] = $pathinfo[2];
3983
            }
3984
            if (array_key_exists(5, $pathinfo)) {
3985
                $ret['extension'] = $pathinfo[5];
3986
            }
3987
            if (array_key_exists(3, $pathinfo)) {
3988
                $ret['filename'] = $pathinfo[3];
3989
            }
3990
        }
3991
        switch ($options) {
3992
            case PATHINFO_DIRNAME:
3993
            case 'dirname':
3994
                return $ret['dirname'];
3995
            case PATHINFO_BASENAME:
3996
            case 'basename':
3997
                return $ret['basename'];
3998
            case PATHINFO_EXTENSION:
3999
            case 'extension':
4000
                return $ret['extension'];
4001
            case PATHINFO_FILENAME:
4002
            case 'filename':
4003
                return $ret['filename'];
4004
            default:
4005
                return $ret;
4006
        }
4007
    }
4008
4009
    /**
4010
     * Set or reset instance properties.
4011
     * You should avoid this function - it's more verbose, less efficient, more error-prone and
4012
     * harder to debug than setting properties directly.
4013
     * Usage Example:
4014
     * `$mail->set('SMTPSecure', 'tls');`
4015
     *   is the same as:
4016
     * `$mail->SMTPSecure = 'tls';`.
4017
     *
4018
     * @param string $name  The property name to set
4019
     * @param mixed  $value The value to set the property to
4020
     *
4021
     * @return bool
4022
     */
4023
    public function set($name, $value = '')
4024
    {
4025
        if (property_exists($this, $name)) {
4026
            $this->$name = $value;
4027
4028
            return true;
4029
        }
4030
        $this->setError($this->lang('variable_set') . $name);
4031
4032
        return false;
4033
    }
4034
4035
    /**
4036
     * Strip newlines to prevent header injection.
4037
     *
4038
     * @param string $str
4039
     *
4040
     * @return string
4041
     */
4042
    public function secureHeader($str)
4043
    {
4044
        return trim(str_replace(["\r", "\n"], '', $str));
4045
    }
4046
4047
    /**
4048
     * Normalize line breaks in a string.
4049
     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
4050
     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
4051
     *
4052
     * @param string $text
4053
     * @param string $breaktype What kind of line break to use; defaults to static::$LE
0 ignored issues
show
Documentation introduced by
Should the type for parameter $breaktype not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

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