PHPMailer::DKIM_Add()   B
last analyzed

Complexity

Conditions 8
Paths 12

Size

Total Lines 72

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 12
nop 3
dl 0
loc 72
rs 7.3664
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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