Passed
Push — main ( 73c905...3e2603 )
by Rafael N.
02:56 queued 41s
created

PHPMailer::createHeader()   F

Complexity

Conditions 22
Paths 7680

Size

Total Lines 84
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 22
eloc 46
c 1
b 0
f 0
nc 7680
nop 0
dl 0
loc 84
rs 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * PHPMailer - PHP email creation and transport class.
5
 * PHP Version 5.5.
6
 *
7
 * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
8
 *
9
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
10
 * @author    Jim Jagielski (jimjag) <[email protected]>
11
 * @author    Andy Prevost (codeworxtech) <[email protected]>
12
 * @author    Brent R. Matzelle (original founder)
13
 * @copyright 2012 - 2020 Marcus Bointon
14
 * @copyright 2010 - 2012 Jim Jagielski
15
 * @copyright 2004 - 2009 Andy Prevost
16
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
17
 * @note      This program is distributed in the hope that it will be useful - WITHOUT
18
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
 * FITNESS FOR A PARTICULAR PURPOSE.
20
 */
21
22
namespace PHPMailer\PHPMailer;
23
24
/**
25
 * PHPMailer - PHP email creation and transport class.
26
 *
27
 * @author Marcus Bointon (Synchro/coolbru) <[email protected]>
28
 * @author Jim Jagielski (jimjag) <[email protected]>
29
 * @author Andy Prevost (codeworxtech) <[email protected]>
30
 * @author Brent R. Matzelle (original founder)
31
 */
32
class PHPMailer
33
{
34
    const CHARSET_ASCII = 'us-ascii';
35
    const CHARSET_ISO88591 = 'iso-8859-1';
36
    const CHARSET_UTF8 = 'utf-8';
37
38
    const CONTENT_TYPE_PLAINTEXT = 'text/plain';
39
    const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';
40
    const CONTENT_TYPE_TEXT_HTML = 'text/html';
41
    const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
42
    const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
43
    const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
44
45
    const ENCODING_7BIT = '7bit';
46
    const ENCODING_8BIT = '8bit';
47
    const ENCODING_BASE64 = 'base64';
48
    const ENCODING_BINARY = 'binary';
49
    const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
50
51
    const ENCRYPTION_STARTTLS = 'tls';
52
    const ENCRYPTION_SMTPS = 'ssl';
53
54
    const ICAL_METHOD_REQUEST = 'REQUEST';
55
    const ICAL_METHOD_PUBLISH = 'PUBLISH';
56
    const ICAL_METHOD_REPLY = 'REPLY';
57
    const ICAL_METHOD_ADD = 'ADD';
58
    const ICAL_METHOD_CANCEL = 'CANCEL';
59
    const ICAL_METHOD_REFRESH = 'REFRESH';
60
    const ICAL_METHOD_COUNTER = 'COUNTER';
61
    const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
62
63
    /**
64
     * Email priority.
65
     * Options: null (default), 1 = High, 3 = Normal, 5 = low.
66
     * When null, the header is not set at all.
67
     *
68
     * @var int|null
69
     */
70
    public $Priority;
71
72
    /**
73
     * The character set of the message.
74
     *
75
     * @var string
76
     */
77
    public $CharSet = self::CHARSET_ISO88591;
78
79
    /**
80
     * The MIME Content-type of the message.
81
     *
82
     * @var string
83
     */
84
    public $ContentType = self::CONTENT_TYPE_PLAINTEXT;
85
86
    /**
87
     * The message encoding.
88
     * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
89
     *
90
     * @var string
91
     */
92
    public $Encoding = self::ENCODING_8BIT;
93
94
    /**
95
     * Holds the most recent mailer error message.
96
     *
97
     * @var string
98
     */
99
    public $ErrorInfo = '';
100
101
    /**
102
     * The From email address for the message.
103
     *
104
     * @var string
105
     */
106
    public $From = 'root@localhost';
107
108
    /**
109
     * The From name of the message.
110
     *
111
     * @var string
112
     */
113
    public $FromName = 'Root User';
114
115
    /**
116
     * The envelope sender of the message.
117
     * This will usually be turned into a Return-Path header by the receiver,
118
     * and is the address that bounces will be sent to.
119
     * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
120
     *
121
     * @var string
122
     */
123
    public $Sender = '';
124
125
    /**
126
     * The Subject of the message.
127
     *
128
     * @var string
129
     */
130
    public $Subject = '';
131
132
    /**
133
     * An HTML or plain text message body.
134
     * If HTML then call isHTML(true).
135
     *
136
     * @var string
137
     */
138
    public $Body = '';
139
140
    /**
141
     * The plain-text message body.
142
     * This body can be read by mail clients that do not have HTML email
143
     * capability such as mutt & Eudora.
144
     * Clients that can read HTML will view the normal Body.
145
     *
146
     * @var string
147
     */
148
    public $AltBody = '';
149
150
    /**
151
     * An iCal message part body.
152
     * Only supported in simple alt or alt_inline message types
153
     * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
154
     *
155
     * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/
156
     * @see http://kigkonsult.se/iCalcreator/
157
     *
158
     * @var string
159
     */
160
    public $Ical = '';
161
162
    /**
163
     * Value-array of "method" in Contenttype header "text/calendar"
164
     *
165
     * @var string[]
166
     */
167
    protected static $IcalMethods = [
168
        self::ICAL_METHOD_REQUEST,
169
        self::ICAL_METHOD_PUBLISH,
170
        self::ICAL_METHOD_REPLY,
171
        self::ICAL_METHOD_ADD,
172
        self::ICAL_METHOD_CANCEL,
173
        self::ICAL_METHOD_REFRESH,
174
        self::ICAL_METHOD_COUNTER,
175
        self::ICAL_METHOD_DECLINECOUNTER,
176
    ];
177
178
    /**
179
     * The complete compiled MIME message body.
180
     *
181
     * @var string
182
     */
183
    protected $MIMEBody = '';
184
185
    /**
186
     * The complete compiled MIME message headers.
187
     *
188
     * @var string
189
     */
190
    protected $MIMEHeader = '';
191
192
    /**
193
     * Extra headers that createHeader() doesn't fold in.
194
     *
195
     * @var string
196
     */
197
    protected $mailHeader = '';
198
199
    /**
200
     * Word-wrap the message body to this number of chars.
201
     * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
202
     *
203
     * @see static::STD_LINE_LENGTH
204
     *
205
     * @var int
206
     */
207
    public $WordWrap = 0;
208
209
    /**
210
     * Which method to use to send mail.
211
     * Options: "mail", "sendmail", or "smtp".
212
     *
213
     * @var string
214
     */
215
    public $Mailer = 'mail';
216
217
    /**
218
     * The path to the sendmail program.
219
     *
220
     * @var string
221
     */
222
    public $Sendmail = '/usr/sbin/sendmail';
223
224
    /**
225
     * Whether mail() uses a fully sendmail-compatible MTA.
226
     * One which supports sendmail's "-oi -f" options.
227
     *
228
     * @var bool
229
     */
230
    public $UseSendmailOptions = true;
231
232
    /**
233
     * The email address that a reading confirmation should be sent to, also known as read receipt.
234
     *
235
     * @var string
236
     */
237
    public $ConfirmReadingTo = '';
238
239
    /**
240
     * The hostname to use in the Message-ID header and as default HELO string.
241
     * If empty, PHPMailer attempts to find one with, in order,
242
     * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
243
     * 'localhost.localdomain'.
244
     *
245
     * @see PHPMailer::$Helo
246
     *
247
     * @var string
248
     */
249
    public $Hostname = '';
250
251
    /**
252
     * An ID to be used in the Message-ID header.
253
     * If empty, a unique id will be generated.
254
     * You can set your own, but it must be in the format "<id@domain>",
255
     * as defined in RFC5322 section 3.6.4 or it will be ignored.
256
     *
257
     * @see https://tools.ietf.org/html/rfc5322#section-3.6.4
258
     *
259
     * @var string
260
     */
261
    public $MessageID = '';
262
263
    /**
264
     * The message Date to be used in the Date header.
265
     * If empty, the current date will be added.
266
     *
267
     * @var string
268
     */
269
    public $MessageDate = '';
270
271
    /**
272
     * SMTP hosts.
273
     * Either a single hostname or multiple semicolon-delimited hostnames.
274
     * You can also specify a different port
275
     * for each host by using this format: [hostname:port]
276
     * (e.g. "smtp1.example.com:25;smtp2.example.com").
277
     * You can also specify encryption type, for example:
278
     * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
279
     * Hosts will be tried in order.
280
     *
281
     * @var string
282
     */
283
    public $Host = 'localhost';
284
285
    /**
286
     * The default SMTP server port.
287
     *
288
     * @var int
289
     */
290
    public $Port = 25;
291
292
    /**
293
     * The SMTP HELO/EHLO name used for the SMTP connection.
294
     * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
295
     * one with the same method described above for $Hostname.
296
     *
297
     * @see PHPMailer::$Hostname
298
     *
299
     * @var string
300
     */
301
    public $Helo = '';
302
303
    /**
304
     * What kind of encryption to use on the SMTP connection.
305
     * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
306
     *
307
     * @var string
308
     */
309
    public $SMTPSecure = '';
310
311
    /**
312
     * Whether to enable TLS encryption automatically if a server supports it,
313
     * even if `SMTPSecure` is not set to 'tls'.
314
     * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
315
     *
316
     * @var bool
317
     */
318
    public $SMTPAutoTLS = true;
319
320
    /**
321
     * Whether to use SMTP authentication.
322
     * Uses the Username and Password properties.
323
     *
324
     * @see PHPMailer::$Username
325
     * @see PHPMailer::$Password
326
     *
327
     * @var bool
328
     */
329
    public $SMTPAuth = false;
330
331
    /**
332
     * Options array passed to stream_context_create when connecting via SMTP.
333
     *
334
     * @var array
335
     */
336
    public $SMTPOptions = [];
337
338
    /**
339
     * SMTP username.
340
     *
341
     * @var string
342
     */
343
    public $Username = '';
344
345
    /**
346
     * SMTP password.
347
     *
348
     * @var string
349
     */
350
    public $Password = '';
351
352
    /**
353
     * SMTP auth type.
354
     * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified.
355
     *
356
     * @var string
357
     */
358
    public $AuthType = '';
359
360
    /**
361
     * An instance of the PHPMailer OAuth class.
362
     *
363
     * @var OAuth
364
     */
365
    protected $oauth;
366
367
    /**
368
     * The SMTP server timeout in seconds.
369
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
370
     *
371
     * @var int
372
     */
373
    public $Timeout = 300;
374
375
    /**
376
     * Comma separated list of DSN notifications
377
     * 'NEVER' under no circumstances a DSN must be returned to the sender.
378
     *         If you use NEVER all other notifications will be ignored.
379
     * 'SUCCESS' will notify you when your mail has arrived at its destination.
380
     * 'FAILURE' will arrive if an error occurred during delivery.
381
     * 'DELAY'   will notify you if there is an unusual delay in delivery, but the actual
382
     *           delivery's outcome (success or failure) is not yet decided.
383
     *
384
     * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
385
     */
386
    public $dsn = '';
387
388
    /**
389
     * SMTP class debug output mode.
390
     * Debug output level.
391
     * Options:
392
     * @see SMTP::DEBUG_OFF: No output
393
     * @see SMTP::DEBUG_CLIENT: Client messages
394
     * @see SMTP::DEBUG_SERVER: Client and server messages
395
     * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
396
     * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
397
     *
398
     * @see SMTP::$do_debug
399
     *
400
     * @var int
401
     */
402
    public $SMTPDebug = 0;
403
404
    /**
405
     * How to handle debug output.
406
     * Options:
407
     * * `echo` Output plain-text as-is, appropriate for CLI
408
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
409
     * * `error_log` Output to error log as configured in php.ini
410
     * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
411
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
412
     *
413
     * ```php
414
     * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
415
     * ```
416
     *
417
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
418
     * level output is used:
419
     *
420
     * ```php
421
     * $mail->Debugoutput = new myPsr3Logger;
422
     * ```
423
     *
424
     * @see SMTP::$Debugoutput
425
     *
426
     * @var string|callable|\Psr\Log\LoggerInterface
0 ignored issues
show
Bug introduced by
The type Psr\Log\LoggerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
427
     */
428
    public $Debugoutput = 'echo';
429
430
    /**
431
     * Whether to keep SMTP connection open after each message.
432
     * If this is set to true then to close the connection
433
     * requires an explicit call to smtpClose().
434
     *
435
     * @var bool
436
     */
437
    public $SMTPKeepAlive = false;
438
439
    /**
440
     * Whether to split multiple to addresses into multiple messages
441
     * or send them all in one message.
442
     * Only supported in `mail` and `sendmail` transports, not in SMTP.
443
     *
444
     * @var bool
445
     *
446
     * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
447
     */
448
    public $SingleTo = false;
449
450
    /**
451
     * Storage for addresses when SingleTo is enabled.
452
     *
453
     * @var array
454
     */
455
    protected $SingleToArray = [];
456
457
    /**
458
     * Whether to generate VERP addresses on send.
459
     * Only applicable when sending via SMTP.
460
     *
461
     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
462
     * @see http://www.postfix.org/VERP_README.html Postfix VERP info
463
     *
464
     * @var bool
465
     */
466
    public $do_verp = false;
467
468
    /**
469
     * Whether to allow sending messages with an empty body.
470
     *
471
     * @var bool
472
     */
473
    public $AllowEmpty = false;
474
475
    /**
476
     * DKIM selector.
477
     *
478
     * @var string
479
     */
480
    public $DKIM_selector = '';
481
482
    /**
483
     * DKIM Identity.
484
     * Usually the email address used as the source of the email.
485
     *
486
     * @var string
487
     */
488
    public $DKIM_identity = '';
489
490
    /**
491
     * DKIM passphrase.
492
     * Used if your key is encrypted.
493
     *
494
     * @var string
495
     */
496
    public $DKIM_passphrase = '';
497
498
    /**
499
     * DKIM signing domain name.
500
     *
501
     * @example 'example.com'
502
     *
503
     * @var string
504
     */
505
    public $DKIM_domain = '';
506
507
    /**
508
     * DKIM Copy header field values for diagnostic use.
509
     *
510
     * @var bool
511
     */
512
    public $DKIM_copyHeaderFields = true;
513
514
    /**
515
     * DKIM Extra signing headers.
516
     *
517
     * @example ['List-Unsubscribe', 'List-Help']
518
     *
519
     * @var array
520
     */
521
    public $DKIM_extraHeaders = [];
522
523
    /**
524
     * DKIM private key file path.
525
     *
526
     * @var string
527
     */
528
    public $DKIM_private = '';
529
530
    /**
531
     * DKIM private key string.
532
     *
533
     * If set, takes precedence over `$DKIM_private`.
534
     *
535
     * @var string
536
     */
537
    public $DKIM_private_string = '';
538
539
    /**
540
     * Callback Action function name.
541
     *
542
     * The function that handles the result of the send email action.
543
     * It is called out by send() for each email sent.
544
     *
545
     * Value can be any php callable: http://www.php.net/is_callable
546
     *
547
     * Parameters:
548
     *   bool $result        result of the send action
549
     *   array   $to            email addresses of the recipients
550
     *   array   $cc            cc email addresses
551
     *   array   $bcc           bcc email addresses
552
     *   string  $subject       the subject
553
     *   string  $body          the email body
554
     *   string  $from          email address of sender
555
     *   string  $extra         extra information of possible use
556
     *                          "smtp_transaction_id' => last smtp transaction id
557
     *
558
     * @var string
559
     */
560
    public $action_function = '';
561
562
    /**
563
     * What to put in the X-Mailer header.
564
     * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
565
     *
566
     * @var string|null
567
     */
568
    public $XMailer = '';
569
570
    /**
571
     * Which validator to use by default when validating email addresses.
572
     * May be a callable to inject your own validator, but there are several built-in validators.
573
     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
574
     *
575
     * @see PHPMailer::validateAddress()
576
     *
577
     * @var string|callable
578
     */
579
    public static $validator = 'php';
580
581
    /**
582
     * An instance of the SMTP sender class.
583
     *
584
     * @var SMTP
585
     */
586
    protected $smtp;
587
588
    /**
589
     * The array of 'to' names and addresses.
590
     *
591
     * @var array
592
     */
593
    protected $to = [];
594
595
    /**
596
     * The array of 'cc' names and addresses.
597
     *
598
     * @var array
599
     */
600
    protected $cc = [];
601
602
    /**
603
     * The array of 'bcc' names and addresses.
604
     *
605
     * @var array
606
     */
607
    protected $bcc = [];
608
609
    /**
610
     * The array of reply-to names and addresses.
611
     *
612
     * @var array
613
     */
614
    protected $ReplyTo = [];
615
616
    /**
617
     * An array of all kinds of addresses.
618
     * Includes all of $to, $cc, $bcc.
619
     *
620
     * @see PHPMailer::$to
621
     * @see PHPMailer::$cc
622
     * @see PHPMailer::$bcc
623
     *
624
     * @var array
625
     */
626
    protected $all_recipients = [];
627
628
    /**
629
     * An array of names and addresses queued for validation.
630
     * In send(), valid and non duplicate entries are moved to $all_recipients
631
     * and one of $to, $cc, or $bcc.
632
     * This array is used only for addresses with IDN.
633
     *
634
     * @see PHPMailer::$to
635
     * @see PHPMailer::$cc
636
     * @see PHPMailer::$bcc
637
     * @see PHPMailer::$all_recipients
638
     *
639
     * @var array
640
     */
641
    protected $RecipientsQueue = [];
642
643
    /**
644
     * An array of reply-to names and addresses queued for validation.
645
     * In send(), valid and non duplicate entries are moved to $ReplyTo.
646
     * This array is used only for addresses with IDN.
647
     *
648
     * @see PHPMailer::$ReplyTo
649
     *
650
     * @var array
651
     */
652
    protected $ReplyToQueue = [];
653
654
    /**
655
     * The array of attachments.
656
     *
657
     * @var array
658
     */
659
    protected $attachment = [];
660
661
    /**
662
     * The array of custom headers.
663
     *
664
     * @var array
665
     */
666
    protected $CustomHeader = [];
667
668
    /**
669
     * The most recent Message-ID (including angular brackets).
670
     *
671
     * @var string
672
     */
673
    protected $lastMessageID = '';
674
675
    /**
676
     * The message's MIME type.
677
     *
678
     * @var string
679
     */
680
    protected $message_type = '';
681
682
    /**
683
     * The array of MIME boundary strings.
684
     *
685
     * @var array
686
     */
687
    protected $boundary = [];
688
689
    /**
690
     * The array of available languages.
691
     *
692
     * @var array
693
     */
694
    protected $language = [];
695
696
    /**
697
     * The number of errors encountered.
698
     *
699
     * @var int
700
     */
701
    protected $error_count = 0;
702
703
    /**
704
     * The S/MIME certificate file path.
705
     *
706
     * @var string
707
     */
708
    protected $sign_cert_file = '';
709
710
    /**
711
     * The S/MIME key file path.
712
     *
713
     * @var string
714
     */
715
    protected $sign_key_file = '';
716
717
    /**
718
     * The optional S/MIME extra certificates ("CA Chain") file path.
719
     *
720
     * @var string
721
     */
722
    protected $sign_extracerts_file = '';
723
724
    /**
725
     * The S/MIME password for the key.
726
     * Used only if the key is encrypted.
727
     *
728
     * @var string
729
     */
730
    protected $sign_key_pass = '';
731
732
    /**
733
     * Whether to throw exceptions for errors.
734
     *
735
     * @var bool
736
     */
737
    protected $exceptions = false;
738
739
    /**
740
     * Unique ID used for message ID and boundaries.
741
     *
742
     * @var string
743
     */
744
    protected $uniqueid = '';
745
746
    /**
747
     * The PHPMailer Version number.
748
     *
749
     * @var string
750
     */
751
    const VERSION = '6.4.0';
752
753
    /**
754
     * Error severity: message only, continue processing.
755
     *
756
     * @var int
757
     */
758
    const STOP_MESSAGE = 0;
759
760
    /**
761
     * Error severity: message, likely ok to continue processing.
762
     *
763
     * @var int
764
     */
765
    const STOP_CONTINUE = 1;
766
767
    /**
768
     * Error severity: message, plus full stop, critical error reached.
769
     *
770
     * @var int
771
     */
772
    const STOP_CRITICAL = 2;
773
774
    /**
775
     * The SMTP standard CRLF line break.
776
     * If you want to change line break format, change static::$LE, not this.
777
     */
778
    const CRLF = "\r\n";
779
780
    /**
781
     * "Folding White Space" a white space string used for line folding.
782
     */
783
    const FWS = ' ';
784
785
    /**
786
     * SMTP RFC standard line ending; Carriage Return, Line Feed.
787
     *
788
     * @var string
789
     */
790
    protected static $LE = self::CRLF;
791
792
    /**
793
     * The maximum line length supported by mail().
794
     *
795
     * Background: mail() will sometimes corrupt messages
796
     * with headers headers longer than 65 chars, see #818.
797
     *
798
     * @var int
799
     */
800
    const MAIL_MAX_LINE_LENGTH = 63;
801
802
    /**
803
     * The maximum line length allowed by RFC 2822 section 2.1.1.
804
     *
805
     * @var int
806
     */
807
    const MAX_LINE_LENGTH = 998;
808
809
    /**
810
     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
811
     * This length does NOT include the line break
812
     * 76 means that lines will be 77 or 78 chars depending on whether
813
     * the line break format is LF or CRLF; both are valid.
814
     *
815
     * @var int
816
     */
817
    const STD_LINE_LENGTH = 76;
818
819
    /**
820
     * Constructor.
821
     *
822
     * @param bool $exceptions Should we throw external exceptions?
823
     */
824
    public function __construct($exceptions = null)
825
    {
826
        if (null !== $exceptions) {
827
            $this->exceptions = (bool) $exceptions;
828
        }
829
        //Pick an appropriate debug output format automatically
830
        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
831
    }
832
833
    /**
834
     * Destructor.
835
     */
836
    public function __destruct()
837
    {
838
        //Close any open SMTP connection nicely
839
        $this->smtpClose();
840
    }
841
842
    /**
843
     * Call mail() in a safe_mode-aware fashion.
844
     * Also, unless sendmail_path points to sendmail (or something that
845
     * claims to be sendmail), don't pass params (not a perfect fix,
846
     * but it will do).
847
     *
848
     * @param string      $to      To
849
     * @param string      $subject Subject
850
     * @param string      $body    Message Body
851
     * @param string      $header  Additional Header(s)
852
     * @param string|null $params  Params
853
     *
854
     * @return bool
855
     */
856
    private function mailPassthru($to, $subject, $body, $header, $params)
857
    {
858
        //Check overloading of mail function to avoid double-encoding
859
        if (ini_get('mbstring.func_overload') & 1) {
860
            $subject = $this->secureHeader($subject);
861
        } else {
862
            $subject = $this->encodeHeader($this->secureHeader($subject));
863
        }
864
        //Calling mail() with null params breaks
865
        $this->edebug('Sending with mail()');
866
        $this->edebug('Sendmail path: ' . ini_get('sendmail_path'));
867
        $this->edebug("Envelope sender: {$this->Sender}");
868
        $this->edebug("To: {$to}");
869
        $this->edebug("Subject: {$subject}");
870
        $this->edebug("Headers: {$header}");
871
        if (!$this->UseSendmailOptions || null === $params) {
872
            $result = @mail($to, $subject, $body, $header);
873
        } else {
874
            $this->edebug("Additional params: {$params}");
875
            $result = @mail($to, $subject, $body, $header, $params);
876
        }
877
        $this->edebug('Result: ' . ($result ? 'true' : 'false'));
878
        return $result;
879
    }
880
881
    /**
882
     * Output debugging info via a user-defined method.
883
     * Only generates output if debug output is enabled.
884
     *
885
     * @see PHPMailer::$Debugoutput
886
     * @see PHPMailer::$SMTPDebug
887
     *
888
     * @param string $str
889
     */
890
    protected function edebug($str)
891
    {
892
        if ($this->SMTPDebug <= 0) {
893
            return;
894
        }
895
        //Is this a PSR-3 logger?
896
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
897
            $this->Debugoutput->debug($str);
898
899
            return;
900
        }
901
        //Avoid clash with built-in function names
902
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
903
            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
904
905
            return;
906
        }
907
        switch ($this->Debugoutput) {
908
            case 'error_log':
909
                //Don't output, just log
910
                /** @noinspection ForgottenDebugOutputInspection */
911
                error_log($str);
912
                break;
913
            case 'html':
914
                //Cleans up output a bit for a better looking, HTML-safe output
915
                echo htmlentities(
916
                    preg_replace('/[\r\n]+/', '', $str),
917
                    ENT_QUOTES,
918
                    'UTF-8'
919
                ), "<br>\n";
920
                break;
921
            case 'echo':
922
            default:
923
                //Normalize line breaks
924
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
925
                echo gmdate('Y-m-d H:i:s'),
926
                "\t",
927
                    //Trim trailing space
928
                trim(
929
                    //Indent for readability, except for trailing break
930
                    str_replace(
931
                        "\n",
932
                        "\n                   \t                  ",
933
                        trim($str)
934
                    )
935
                ),
936
                "\n";
937
        }
938
    }
939
940
    /**
941
     * Sets message type to HTML or plain.
942
     *
943
     * @param bool $isHtml True for HTML mode
944
     */
945
    public function isHTML($isHtml = true)
946
    {
947
        if ($isHtml) {
948
            $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
949
        } else {
950
            $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
951
        }
952
    }
953
954
    /**
955
     * Send messages using SMTP.
956
     */
957
    public function isSMTP()
958
    {
959
        $this->Mailer = 'smtp';
960
    }
961
962
    /**
963
     * Send messages using PHP's mail() function.
964
     */
965
    public function isMail()
966
    {
967
        $this->Mailer = 'mail';
968
    }
969
970
    /**
971
     * Send messages using $Sendmail.
972
     */
973
    public function isSendmail()
974
    {
975
        $ini_sendmail_path = ini_get('sendmail_path');
976
977
        if (false === stripos($ini_sendmail_path, 'sendmail')) {
978
            $this->Sendmail = '/usr/sbin/sendmail';
979
        } else {
980
            $this->Sendmail = $ini_sendmail_path;
981
        }
982
        $this->Mailer = 'sendmail';
983
    }
984
985
    /**
986
     * Send messages using qmail.
987
     */
988
    public function isQmail()
989
    {
990
        $ini_sendmail_path = ini_get('sendmail_path');
991
992
        if (false === stripos($ini_sendmail_path, 'qmail')) {
993
            $this->Sendmail = '/var/qmail/bin/qmail-inject';
994
        } else {
995
            $this->Sendmail = $ini_sendmail_path;
996
        }
997
        $this->Mailer = 'qmail';
998
    }
999
1000
    /**
1001
     * Add a "To" address.
1002
     *
1003
     * @param string $address The email address to send to
1004
     * @param string $name
1005
     *
1006
     * @throws Exception
1007
     *
1008
     * @return bool true on success, false if address already used or invalid in some way
1009
     */
1010
    public function addAddress($address, $name = '')
1011
    {
1012
        return $this->addOrEnqueueAnAddress('to', $address, $name);
1013
    }
1014
1015
    /**
1016
     * Add a "CC" address.
1017
     *
1018
     * @param string $address The email address to send to
1019
     * @param string $name
1020
     *
1021
     * @throws Exception
1022
     *
1023
     * @return bool true on success, false if address already used or invalid in some way
1024
     */
1025
    public function addCC($address, $name = '')
1026
    {
1027
        return $this->addOrEnqueueAnAddress('cc', $address, $name);
1028
    }
1029
1030
    /**
1031
     * Add a "BCC" address.
1032
     *
1033
     * @param string $address The email address to send to
1034
     * @param string $name
1035
     *
1036
     * @throws Exception
1037
     *
1038
     * @return bool true on success, false if address already used or invalid in some way
1039
     */
1040
    public function addBCC($address, $name = '')
1041
    {
1042
        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
1043
    }
1044
1045
    /**
1046
     * Add a "Reply-To" address.
1047
     *
1048
     * @param string $address The email address to reply to
1049
     * @param string $name
1050
     *
1051
     * @throws Exception
1052
     *
1053
     * @return bool true on success, false if address already used or invalid in some way
1054
     */
1055
    public function addReplyTo($address, $name = '')
1056
    {
1057
        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
1058
    }
1059
1060
    /**
1061
     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
1062
     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
1063
     * be modified after calling this function), addition of such addresses is delayed until send().
1064
     * Addresses that have been added already return false, but do not throw exceptions.
1065
     *
1066
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1067
     * @param string $address The email address to send, resp. to reply to
1068
     * @param string $name
1069
     *
1070
     * @throws Exception
1071
     *
1072
     * @return bool true on success, false if address already used or invalid in some way
1073
     */
1074
    protected function addOrEnqueueAnAddress($kind, $address, $name)
1075
    {
1076
        $address = trim($address);
1077
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1078
        $pos = strrpos($address, '@');
1079
        if (false === $pos) {
1080
            //At-sign is missing.
1081
            $error_message = sprintf(
1082
                '%s (%s): %s',
1083
                $this->lang('invalid_address'),
1084
                $kind,
1085
                $address
1086
            );
1087
            $this->setError($error_message);
1088
            $this->edebug($error_message);
1089
            if ($this->exceptions) {
1090
                throw new Exception($error_message);
1091
            }
1092
1093
            return false;
1094
        }
1095
        $params = [$kind, $address, $name];
1096
        //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
1097
        if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
1098
            if ('Reply-To' !== $kind) {
1099
                if (!array_key_exists($address, $this->RecipientsQueue)) {
1100
                    $this->RecipientsQueue[$address] = $params;
1101
1102
                    return true;
1103
                }
1104
            } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
1105
                $this->ReplyToQueue[$address] = $params;
1106
1107
                return true;
1108
            }
1109
1110
            return false;
1111
        }
1112
1113
        //Immediately add standard addresses without IDN.
1114
        return call_user_func_array([$this, 'addAnAddress'], $params);
1115
    }
1116
1117
    /**
1118
     * Add an address to one of the recipient arrays or to the ReplyTo array.
1119
     * Addresses that have been added already return false, but do not throw exceptions.
1120
     *
1121
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1122
     * @param string $address The email address to send, resp. to reply to
1123
     * @param string $name
1124
     *
1125
     * @throws Exception
1126
     *
1127
     * @return bool true on success, false if address already used or invalid in some way
1128
     */
1129
    protected function addAnAddress($kind, $address, $name = '')
1130
    {
1131
        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1132
            $error_message = sprintf(
1133
                '%s: %s',
1134
                $this->lang('Invalid recipient kind'),
1135
                $kind
1136
            );
1137
            $this->setError($error_message);
1138
            $this->edebug($error_message);
1139
            if ($this->exceptions) {
1140
                throw new Exception($error_message);
1141
            }
1142
1143
            return false;
1144
        }
1145
        if (!static::validateAddress($address)) {
1146
            $error_message = sprintf(
1147
                '%s (%s): %s',
1148
                $this->lang('invalid_address'),
1149
                $kind,
1150
                $address
1151
            );
1152
            $this->setError($error_message);
1153
            $this->edebug($error_message);
1154
            if ($this->exceptions) {
1155
                throw new Exception($error_message);
1156
            }
1157
1158
            return false;
1159
        }
1160
        if ('Reply-To' !== $kind) {
1161
            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
1162
                $this->{$kind}[] = [$address, $name];
1163
                $this->all_recipients[strtolower($address)] = true;
1164
1165
                return true;
1166
            }
1167
        } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
1168
            $this->ReplyTo[strtolower($address)] = [$address, $name];
1169
1170
            return true;
1171
        }
1172
1173
        return false;
1174
    }
1175
1176
    /**
1177
     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1178
     * of the form "display name <address>" into an array of name/address pairs.
1179
     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1180
     * Note that quotes in the name part are removed.
1181
     *
1182
     * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1183
     *
1184
     * @param string $addrstr The address list string
1185
     * @param bool   $useimap Whether to use the IMAP extension to parse the list
1186
     *
1187
     * @return array
1188
     */
1189
    public static function parseAddresses($addrstr, $useimap = true)
1190
    {
1191
        $addresses = [];
1192
        if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
1193
            //Use this built-in parser if it's available
1194
            $list = imap_rfc822_parse_adrlist($addrstr, '');
1195
            foreach ($list as $address) {
1196
                if (
1197
                    ('.SYNTAX-ERROR.' !== $address->host) && static::validateAddress(
1198
                        $address->mailbox . '@' . $address->host
1199
                    )
1200
                ) {
1201
                    //Decode the name part if it's present and encoded
1202
                    if (
1203
                        property_exists($address, 'personal') &&
1204
                        extension_loaded('mbstring') &&
1205
                        preg_match('/^=\?.*\?=$/', $address->personal)
1206
                    ) {
1207
                        $address->personal = mb_decode_mimeheader($address->personal);
1208
                    }
1209
1210
                    $addresses[] = [
1211
                        'name' => (property_exists($address, 'personal') ? $address->personal : ''),
1212
                        'address' => $address->mailbox . '@' . $address->host,
1213
                    ];
1214
                }
1215
            }
1216
        } else {
1217
            //Use this simpler parser
1218
            $list = explode(',', $addrstr);
1219
            foreach ($list as $address) {
1220
                $address = trim($address);
1221
                //Is there a separate name part?
1222
                if (strpos($address, '<') === false) {
1223
                    //No separate name, just use the whole thing
1224
                    if (static::validateAddress($address)) {
1225
                        $addresses[] = [
1226
                            'name' => '',
1227
                            'address' => $address,
1228
                        ];
1229
                    }
1230
                } else {
1231
                    list($name, $email) = explode('<', $address);
1232
                    $email = trim(str_replace('>', '', $email));
1233
                    $name = trim($name);
1234
                    if (static::validateAddress($email)) {
1235
                        //If this name is encoded, decode it
1236
                        if (preg_match('/^=\?.*\?=$/', $name)) {
1237
                            $name = mb_decode_mimeheader($name);
1238
                        }
1239
                        $addresses[] = [
1240
                            //Remove any surrounding quotes and spaces from the name
1241
                            'name' => trim($name, '\'" '),
1242
                            'address' => $email,
1243
                        ];
1244
                    }
1245
                }
1246
            }
1247
        }
1248
1249
        return $addresses;
1250
    }
1251
1252
    /**
1253
     * Set the From and FromName properties.
1254
     *
1255
     * @param string $address
1256
     * @param string $name
1257
     * @param bool   $auto    Whether to also set the Sender address, defaults to true
1258
     *
1259
     * @throws Exception
1260
     *
1261
     * @return bool
1262
     */
1263
    public function setFrom($address, $name = '', $auto = true)
1264
    {
1265
        $address = trim($address);
1266
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1267
        //Don't validate now addresses with IDN. Will be done in send().
1268
        $pos = strrpos($address, '@');
1269
        if (
1270
            (false === $pos)
1271
            || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
1272
            && !static::validateAddress($address))
1273
        ) {
1274
            $error_message = sprintf(
1275
                '%s (From): %s',
1276
                $this->lang('invalid_address'),
1277
                $address
1278
            );
1279
            $this->setError($error_message);
1280
            $this->edebug($error_message);
1281
            if ($this->exceptions) {
1282
                throw new Exception($error_message);
1283
            }
1284
1285
            return false;
1286
        }
1287
        $this->From = $address;
1288
        $this->FromName = $name;
1289
        if ($auto && empty($this->Sender)) {
1290
            $this->Sender = $address;
1291
        }
1292
1293
        return true;
1294
    }
1295
1296
    /**
1297
     * Return the Message-ID header of the last email.
1298
     * Technically this is the value from the last time the headers were created,
1299
     * but it's also the message ID of the last sent message except in
1300
     * pathological cases.
1301
     *
1302
     * @return string
1303
     */
1304
    public function getLastMessageID()
1305
    {
1306
        return $this->lastMessageID;
1307
    }
1308
1309
    /**
1310
     * Check that a string looks like an email address.
1311
     * Validation patterns supported:
1312
     * * `auto` Pick best pattern automatically;
1313
     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
1314
     * * `pcre` Use old PCRE implementation;
1315
     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
1316
     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
1317
     * * `noregex` Don't use a regex: super fast, really dumb.
1318
     * Alternatively you may pass in a callable to inject your own validator, for example:
1319
     *
1320
     * ```php
1321
     * PHPMailer::validateAddress('[email protected]', function($address) {
1322
     *     return (strpos($address, '@') !== false);
1323
     * });
1324
     * ```
1325
     *
1326
     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
1327
     *
1328
     * @param string          $address       The email address to check
1329
     * @param string|callable $patternselect Which pattern to use
1330
     *
1331
     * @return bool
1332
     */
1333
    public static function validateAddress($address, $patternselect = null)
1334
    {
1335
        if (null === $patternselect) {
1336
            $patternselect = static::$validator;
1337
        }
1338
        if (is_callable($patternselect)) {
1339
            return call_user_func($patternselect, $address);
1340
        }
1341
        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
1342
        if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
1343
            return false;
1344
        }
1345
        switch ($patternselect) {
1346
            case 'pcre': //Kept for BC
1347
            case 'pcre8':
1348
                /*
1349
                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
1350
                 * is based.
1351
                 * In addition to the addresses allowed by filter_var, also permits:
1352
                 *  * dotless domains: `a@b`
1353
                 *  * comments: `1234 @ local(blah) .machine .example`
1354
                 *  * quoted elements: `'"test blah"@example.org'`
1355
                 *  * numeric TLDs: `[email protected]`
1356
                 *  * unbracketed IPv4 literals: `[email protected]`
1357
                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'
1358
                 * Not all of these will necessarily work for sending!
1359
                 *
1360
                 * @see       http://squiloople.com/2009/12/20/email-address-validation/
1361
                 * @copyright 2009-2010 Michael Rushton
1362
                 * Feel free to use and redistribute this code. But please keep this copyright notice.
1363
                 */
1364
                return (bool) preg_match(
1365
                    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
1366
                    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
1367
                    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
1368
                    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
1369
                    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
1370
                    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
1371
                    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
1372
                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
1373
                    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
1374
                    $address
1375
                );
1376
            case 'html5':
1377
                /*
1378
                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
1379
                 *
1380
                 * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
1381
                 */
1382
                return (bool) preg_match(
1383
                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
1384
                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
1385
                    $address
1386
                );
1387
            case 'php':
1388
            default:
1389
                return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
1390
        }
1391
    }
1392
1393
    /**
1394
     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
1395
     * `intl` and `mbstring` PHP extensions.
1396
     *
1397
     * @return bool `true` if required functions for IDN support are present
1398
     */
1399
    public static function idnSupported()
1400
    {
1401
        return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
1402
    }
1403
1404
    /**
1405
     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
1406
     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
1407
     * This function silently returns unmodified address if:
1408
     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
1409
     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
1410
     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).
1411
     *
1412
     * @see PHPMailer::$CharSet
1413
     *
1414
     * @param string $address The email address to convert
1415
     *
1416
     * @return string The encoded address in ASCII form
1417
     */
1418
    public function punyencodeAddress($address)
1419
    {
1420
        //Verify we have required functions, CharSet, and at-sign.
1421
        $pos = strrpos($address, '@');
1422
        if (
1423
            !empty($this->CharSet) &&
1424
            false !== $pos &&
1425
            static::idnSupported()
1426
        ) {
1427
            $domain = substr($address, ++$pos);
1428
            //Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
1429
            if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
1430
                //Convert the domain from whatever charset it's in to UTF-8
1431
                $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);
1432
                //Ignore IDE complaints about this line - method signature changed in PHP 5.4
1433
                $errorcode = 0;
1434
                if (defined('INTL_IDNA_VARIANT_UTS46')) {
1435
                    //Use the current punycode standard (appeared in PHP 7.2)
1436
                    $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_UTS46);
0 ignored issues
show
Bug introduced by
It seems like $domain can also be of type array; however, parameter $domain of idn_to_ascii() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1436
                    $punycode = idn_to_ascii(/** @scrutinizer ignore-type */ $domain, $errorcode, \INTL_IDNA_VARIANT_UTS46);
Loading history...
1437
                } elseif (defined('INTL_IDNA_VARIANT_2003')) {
1438
                    //Fall back to this old, deprecated/removed encoding
1439
                    $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003);
0 ignored issues
show
introduced by
The constant INTL_IDNA_VARIANT_2003 has been deprecated: 7.2 Use {@see INTL_IDNA_VARIANT_UTS46} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1439
                    $punycode = idn_to_ascii($domain, $errorcode, /** @scrutinizer ignore-deprecated */ \INTL_IDNA_VARIANT_2003);
Loading history...
1440
                } else {
1441
                    //Fall back to a default we don't know about
1442
                    $punycode = idn_to_ascii($domain, $errorcode);
1443
                }
1444
                if (false !== $punycode) {
1445
                    return substr($address, 0, $pos) . $punycode;
1446
                }
1447
            }
1448
        }
1449
1450
        return $address;
1451
    }
1452
1453
    /**
1454
     * Create a message and send it.
1455
     * Uses the sending method specified by $Mailer.
1456
     *
1457
     * @throws Exception
1458
     *
1459
     * @return bool false on error - See the ErrorInfo property for details of the error
1460
     */
1461
    public function send()
1462
    {
1463
        try {
1464
            if (!$this->preSend()) {
1465
                return false;
1466
            }
1467
1468
            return $this->postSend();
1469
        } catch (Exception $exc) {
1470
            $this->mailHeader = '';
1471
            $this->setError($exc->getMessage());
1472
            if ($this->exceptions) {
1473
                throw $exc;
1474
            }
1475
1476
            return false;
1477
        }
1478
    }
1479
1480
    /**
1481
     * Prepare a message for sending.
1482
     *
1483
     * @throws Exception
1484
     *
1485
     * @return bool
1486
     */
1487
    public function preSend()
1488
    {
1489
        if (
1490
            'smtp' === $this->Mailer
1491
            || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
1492
        ) {
1493
            //SMTP mandates RFC-compliant line endings
1494
            //and it's also used with mail() on Windows
1495
            static::setLE(self::CRLF);
1496
        } else {
1497
            //Maintain backward compatibility with legacy Linux command line mailers
1498
            static::setLE(PHP_EOL);
1499
        }
1500
        //Check for buggy PHP versions that add a header with an incorrect line break
1501
        if (
1502
            'mail' === $this->Mailer
1503
            && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
1504
                || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
1505
            && ini_get('mail.add_x_header') === '1'
1506
            && stripos(PHP_OS, 'WIN') === 0
1507
        ) {
1508
            trigger_error(
1509
                'Your version of PHP is affected by a bug that may result in corrupted messages.' .
1510
                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
1511
                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
1512
                E_USER_WARNING
1513
            );
1514
        }
1515
1516
        try {
1517
            $this->error_count = 0; //Reset errors
1518
            $this->mailHeader = '';
1519
1520
            //Dequeue recipient and Reply-To addresses with IDN
1521
            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
1522
                $params[1] = $this->punyencodeAddress($params[1]);
1523
                call_user_func_array([$this, 'addAnAddress'], $params);
1524
            }
1525
            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
1526
                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
1527
            }
1528
1529
            //Validate From, Sender, and ConfirmReadingTo addresses
1530
            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
1531
                $this->$address_kind = trim($this->$address_kind);
1532
                if (empty($this->$address_kind)) {
1533
                    continue;
1534
                }
1535
                $this->$address_kind = $this->punyencodeAddress($this->$address_kind);
1536
                if (!static::validateAddress($this->$address_kind)) {
1537
                    $error_message = sprintf(
1538
                        '%s (%s): %s',
1539
                        $this->lang('invalid_address'),
1540
                        $address_kind,
1541
                        $this->$address_kind
1542
                    );
1543
                    $this->setError($error_message);
1544
                    $this->edebug($error_message);
1545
                    if ($this->exceptions) {
1546
                        throw new Exception($error_message);
1547
                    }
1548
1549
                    return false;
1550
                }
1551
            }
1552
1553
            //Set whether the message is multipart/alternative
1554
            if ($this->alternativeExists()) {
1555
                $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
1556
            }
1557
1558
            $this->setMessageType();
1559
            //Refuse to send an empty message unless we are specifically allowing it
1560
            if (!$this->AllowEmpty && empty($this->Body)) {
1561
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
1562
            }
1563
1564
            //Trim subject consistently
1565
            $this->Subject = trim($this->Subject);
1566
            //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
1567
            $this->MIMEHeader = '';
1568
            $this->MIMEBody = $this->createBody();
1569
            //createBody may have added some headers, so retain them
1570
            $tempheaders = $this->MIMEHeader;
1571
            $this->MIMEHeader = $this->createHeader();
1572
            $this->MIMEHeader .= $tempheaders;
1573
1574
            //To capture the complete message when using mail(), create
1575
            //an extra header list which createHeader() doesn't fold in
1576
            if ('mail' === $this->Mailer) {
1577
                if (count($this->to) > 0) {
1578
                    $this->mailHeader .= $this->addrAppend('To', $this->to);
1579
                } else {
1580
                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
1581
                }
1582
                $this->mailHeader .= $this->headerLine(
1583
                    'Subject',
1584
                    $this->encodeHeader($this->secureHeader($this->Subject))
1585
                );
1586
            }
1587
1588
            //Sign with DKIM if enabled
1589
            if (
1590
                !empty($this->DKIM_domain)
1591
                && !empty($this->DKIM_selector)
1592
                && (!empty($this->DKIM_private_string)
1593
                    || (!empty($this->DKIM_private)
1594
                        && static::isPermittedPath($this->DKIM_private)
1595
                        && file_exists($this->DKIM_private)
1596
                    )
1597
                )
1598
            ) {
1599
                $header_dkim = $this->DKIM_Add(
1600
                    $this->MIMEHeader . $this->mailHeader,
1601
                    $this->encodeHeader($this->secureHeader($this->Subject)),
1602
                    $this->MIMEBody
1603
                );
1604
                $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .
1605
                    static::normalizeBreaks($header_dkim) . static::$LE;
1606
            }
1607
1608
            return true;
1609
        } catch (Exception $exc) {
1610
            $this->setError($exc->getMessage());
1611
            if ($this->exceptions) {
1612
                throw $exc;
1613
            }
1614
1615
            return false;
1616
        }
1617
    }
1618
1619
    /**
1620
     * Actually send a message via the selected mechanism.
1621
     *
1622
     * @throws Exception
1623
     *
1624
     * @return bool
1625
     */
1626
    public function postSend()
1627
    {
1628
        try {
1629
            //Choose the mailer and send through it
1630
            switch ($this->Mailer) {
1631
                case 'sendmail':
1632
                case 'qmail':
1633
                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
1634
                case 'smtp':
1635
                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
1636
                case 'mail':
1637
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1638
                default:
1639
                    $sendMethod = $this->Mailer . 'Send';
1640
                    if (method_exists($this, $sendMethod)) {
1641
                        return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody);
1642
                    }
1643
1644
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1645
            }
1646
        } catch (Exception $exc) {
1647
            if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1648
                $this->smtp->reset();
1649
            }
1650
            $this->setError($exc->getMessage());
1651
            $this->edebug($exc->getMessage());
1652
            if ($this->exceptions) {
1653
                throw $exc;
1654
            }
1655
        }
1656
1657
        return false;
1658
    }
1659
1660
    /**
1661
     * Send mail using the $Sendmail program.
1662
     *
1663
     * @see PHPMailer::$Sendmail
1664
     *
1665
     * @param string $header The message headers
1666
     * @param string $body   The message body
1667
     *
1668
     * @throws Exception
1669
     *
1670
     * @return bool
1671
     */
1672
    protected function sendmailSend($header, $body)
1673
    {
1674
        if ($this->Mailer === 'qmail') {
1675
            $this->edebug('Sending with qmail');
1676
        } else {
1677
            $this->edebug('Sending with sendmail');
1678
        }
1679
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1680
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1681
        //A space after `-f` is optional, but there is a long history of its presence
1682
        //causing problems, so we don't use one
1683
        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1684
        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
1685
        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
1686
        //Example problem: https://www.drupal.org/node/1057954
1687
        if (empty($this->Sender) && !empty(ini_get('sendmail_from'))) {
1688
            //PHP config has a sender address we can use
1689
            $this->Sender = ini_get('sendmail_from');
1690
        }
1691
        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1692
        if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
1693
            if ($this->Mailer === 'qmail') {
1694
                $sendmailFmt = '%s -f%s';
1695
            } else {
1696
                $sendmailFmt = '%s -oi -f%s -t';
1697
            }
1698
        } else {
1699
            //allow sendmail to choose a default envelope sender. It may
1700
            //seem preferable to force it to use the From header as with
1701
            //SMTP, but that introduces new problems (see
1702
            //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and
1703
            //it has historically worked this way.
1704
            $sendmailFmt = '%s -oi -t';
1705
        }
1706
1707
        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
1708
        $this->edebug('Sendmail path: ' . $this->Sendmail);
1709
        $this->edebug('Sendmail command: ' . $sendmail);
1710
        $this->edebug('Envelope sender: ' . $this->Sender);
1711
        $this->edebug("Headers: {$header}");
1712
1713
        if ($this->SingleTo) {
0 ignored issues
show
Deprecated Code introduced by
The property PHPMailer\PHPMailer\PHPMailer::$SingleTo has been deprecated: 6.0.0 PHPMailer isn't a mailing list manager! ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1713
        if (/** @scrutinizer ignore-deprecated */ $this->SingleTo) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1714
            foreach ($this->SingleToArray as $toAddr) {
1715
                $mail = @popen($sendmail, 'w');
1716
                if (!$mail) {
1717
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1718
                }
1719
                $this->edebug("To: {$toAddr}");
1720
                fwrite($mail, 'To: ' . $toAddr . "\n");
1721
                fwrite($mail, $header);
1722
                fwrite($mail, $body);
1723
                $result = pclose($mail);
1724
                $this->doCallback(
1725
                    ($result === 0),
1726
                    [$toAddr],
1727
                    $this->cc,
1728
                    $this->bcc,
1729
                    $this->Subject,
1730
                    $body,
1731
                    $this->From,
1732
                    []
1733
                );
1734
                $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
1735
                if (0 !== $result) {
1736
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1737
                }
1738
            }
1739
        } else {
1740
            $mail = @popen($sendmail, 'w');
1741
            if (!$mail) {
0 ignored issues
show
introduced by
$mail is of type false|resource, thus it always evaluated to false.
Loading history...
1742
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1743
            }
1744
            fwrite($mail, $header);
1745
            fwrite($mail, $body);
1746
            $result = pclose($mail);
1747
            $this->doCallback(
1748
                ($result === 0),
1749
                $this->to,
1750
                $this->cc,
1751
                $this->bcc,
1752
                $this->Subject,
1753
                $body,
1754
                $this->From,
1755
                []
1756
            );
1757
            $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
1758
            if (0 !== $result) {
1759
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1760
            }
1761
        }
1762
1763
        return true;
1764
    }
1765
1766
    /**
1767
     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
1768
     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
1769
     *
1770
     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
1771
     *
1772
     * @param string $string The string to be validated
1773
     *
1774
     * @return bool
1775
     */
1776
    protected static function isShellSafe($string)
1777
    {
1778
        //Future-proof
1779
        if (
1780
            escapeshellcmd($string) !== $string
1781
            || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
1782
        ) {
1783
            return false;
1784
        }
1785
1786
        $length = strlen($string);
1787
1788
        for ($i = 0; $i < $length; ++$i) {
1789
            $c = $string[$i];
1790
1791
            //All other characters have a special meaning in at least one common shell, including = and +.
1792
            //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
1793
            //Note that this does permit non-Latin alphanumeric characters based on the current locale.
1794
            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
1795
                return false;
1796
            }
1797
        }
1798
1799
        return true;
1800
    }
1801
1802
    /**
1803
     * Check whether a file path is of a permitted type.
1804
     * Used to reject URLs and phar files from functions that access local file paths,
1805
     * such as addAttachment.
1806
     *
1807
     * @param string $path A relative or absolute path to a file
1808
     *
1809
     * @return bool
1810
     */
1811
    protected static function isPermittedPath($path)
1812
    {
1813
        return !preg_match('#^[a-z]+://#i', $path);
1814
    }
1815
1816
    /**
1817
     * Check whether a file path is safe, accessible, and readable.
1818
     *
1819
     * @param string $path A relative or absolute path to a file
1820
     *
1821
     * @return bool
1822
     */
1823
    protected static function fileIsAccessible($path)
1824
    {
1825
        $readable = file_exists($path);
1826
        //If not a UNC path (expected to start with \\), check read permission, see #2069
1827
        if (strpos($path, '\\\\') !== 0) {
1828
            $readable = $readable && is_readable($path);
1829
        }
1830
        return static::isPermittedPath($path) && $readable;
1831
    }
1832
1833
    /**
1834
     * Send mail using the PHP mail() function.
1835
     *
1836
     * @see http://www.php.net/manual/en/book.mail.php
1837
     *
1838
     * @param string $header The message headers
1839
     * @param string $body   The message body
1840
     *
1841
     * @throws Exception
1842
     *
1843
     * @return bool
1844
     */
1845
    protected function mailSend($header, $body)
1846
    {
1847
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1848
1849
        $toArr = [];
1850
        foreach ($this->to as $toaddr) {
1851
            $toArr[] = $this->addrFormat($toaddr);
1852
        }
1853
        $to = implode(', ', $toArr);
1854
1855
        $params = null;
1856
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1857
        //A space after `-f` is optional, but there is a long history of its presence
1858
        //causing problems, so we don't use one
1859
        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1860
        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
1861
        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
1862
        //Example problem: https://www.drupal.org/node/1057954
1863
        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1864
        if (empty($this->Sender) && !empty(ini_get('sendmail_from'))) {
1865
            //PHP config has a sender address we can use
1866
            $this->Sender = ini_get('sendmail_from');
1867
        }
1868
        if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
1869
            if (self::isShellSafe($this->Sender)) {
1870
                $params = sprintf('-f%s', $this->Sender);
1871
            }
1872
            $old_from = ini_get('sendmail_from');
1873
            ini_set('sendmail_from', $this->Sender);
1874
        }
1875
        $result = false;
1876
        if ($this->SingleTo && count($toArr) > 1) {
0 ignored issues
show
Deprecated Code introduced by
The property PHPMailer\PHPMailer\PHPMailer::$SingleTo has been deprecated: 6.0.0 PHPMailer isn't a mailing list manager! ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1876
        if (/** @scrutinizer ignore-deprecated */ $this->SingleTo && count($toArr) > 1) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1877
            foreach ($toArr as $toAddr) {
1878
                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
1879
                $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
1880
            }
1881
        } else {
1882
            $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
1883
            $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
1884
        }
1885
        if (isset($old_from)) {
1886
            ini_set('sendmail_from', $old_from);
1887
        }
1888
        if (!$result) {
1889
            throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
1890
        }
1891
1892
        return true;
1893
    }
1894
1895
    /**
1896
     * Get an instance to use for SMTP operations.
1897
     * Override this function to load your own SMTP implementation,
1898
     * or set one with setSMTPInstance.
1899
     *
1900
     * @return SMTP
1901
     */
1902
    public function getSMTPInstance()
1903
    {
1904
        if (!is_object($this->smtp)) {
1905
            $this->smtp = new SMTP();
1906
        }
1907
1908
        return $this->smtp;
1909
    }
1910
1911
    /**
1912
     * Provide an instance to use for SMTP operations.
1913
     *
1914
     * @return SMTP
1915
     */
1916
    public function setSMTPInstance(SMTP $smtp)
1917
    {
1918
        $this->smtp = $smtp;
1919
1920
        return $this->smtp;
1921
    }
1922
1923
    /**
1924
     * Send mail via SMTP.
1925
     * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
1926
     *
1927
     * @see PHPMailer::setSMTPInstance() to use a different class.
1928
     *
1929
     * @uses \PHPMailer\PHPMailer\SMTP
1930
     *
1931
     * @param string $header The message headers
1932
     * @param string $body   The message body
1933
     *
1934
     * @throws Exception
1935
     *
1936
     * @return bool
1937
     */
1938
    protected function smtpSend($header, $body)
1939
    {
1940
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1941
        $bad_rcpt = [];
1942
        if (!$this->smtpConnect($this->SMTPOptions)) {
1943
            throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
1944
        }
1945
        //Sender already validated in preSend()
1946
        if ('' === $this->Sender) {
1947
            $smtp_from = $this->From;
1948
        } else {
1949
            $smtp_from = $this->Sender;
1950
        }
1951
        if (!$this->smtp->mail($smtp_from)) {
1952
            $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
1953
            throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
1954
        }
1955
1956
        $callbacks = [];
1957
        //Attempt to send to all recipients
1958
        foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
1959
            foreach ($togroup as $to) {
1960
                if (!$this->smtp->recipient($to[0], $this->dsn)) {
1961
                    $error = $this->smtp->getError();
1962
                    $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
1963
                    $isSent = false;
1964
                } else {
1965
                    $isSent = true;
1966
                }
1967
1968
                $callbacks[] = ['issent' => $isSent, 'to' => $to[0]];
1969
            }
1970
        }
1971
1972
        //Only send the DATA command if we have viable recipients
1973
        if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
1974
            throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
1975
        }
1976
1977
        $smtp_transaction_id = $this->smtp->getLastTransactionID();
1978
1979
        if ($this->SMTPKeepAlive) {
1980
            $this->smtp->reset();
1981
        } else {
1982
            $this->smtp->quit();
1983
            $this->smtp->close();
1984
        }
1985
1986
        foreach ($callbacks as $cb) {
1987
            $this->doCallback(
1988
                $cb['issent'],
1989
                [$cb['to']],
1990
                [],
1991
                [],
1992
                $this->Subject,
1993
                $body,
1994
                $this->From,
1995
                ['smtp_transaction_id' => $smtp_transaction_id]
1996
            );
1997
        }
1998
1999
        //Create error message for any bad addresses
2000
        if (count($bad_rcpt) > 0) {
2001
            $errstr = '';
2002
            foreach ($bad_rcpt as $bad) {
2003
                $errstr .= $bad['to'] . ': ' . $bad['error'];
2004
            }
2005
            throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE);
2006
        }
2007
2008
        return true;
2009
    }
2010
2011
    /**
2012
     * Initiate a connection to an SMTP server.
2013
     * Returns false if the operation failed.
2014
     *
2015
     * @param array $options An array of options compatible with stream_context_create()
2016
     *
2017
     * @throws Exception
2018
     *
2019
     * @uses \PHPMailer\PHPMailer\SMTP
2020
     *
2021
     * @return bool
2022
     */
2023
    public function smtpConnect($options = null)
2024
    {
2025
        if (null === $this->smtp) {
2026
            $this->smtp = $this->getSMTPInstance();
2027
        }
2028
2029
        //If no options are provided, use whatever is set in the instance
2030
        if (null === $options) {
2031
            $options = $this->SMTPOptions;
2032
        }
2033
2034
        //Already connected?
2035
        if ($this->smtp->connected()) {
2036
            return true;
2037
        }
2038
2039
        $this->smtp->setTimeout($this->Timeout);
2040
        $this->smtp->setDebugLevel($this->SMTPDebug);
2041
        $this->smtp->setDebugOutput($this->Debugoutput);
2042
        $this->smtp->setVerp($this->do_verp);
2043
        $hosts = explode(';', $this->Host);
2044
        $lastexception = null;
2045
2046
        foreach ($hosts as $hostentry) {
2047
            $hostinfo = [];
2048
            if (
2049
                !preg_match(
2050
                    '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
2051
                    trim($hostentry),
2052
                    $hostinfo
2053
                )
2054
            ) {
2055
                $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
2056
                //Not a valid host entry
2057
                continue;
2058
            }
2059
            //$hostinfo[1]: optional ssl or tls prefix
2060
            //$hostinfo[2]: the hostname
2061
            //$hostinfo[3]: optional port number
2062
            //The host string prefix can temporarily override the current setting for SMTPSecure
2063
            //If it's not specified, the default value is used
2064
2065
            //Check the host name is a valid name or IP address before trying to use it
2066
            if (!static::isValidHost($hostinfo[2])) {
2067
                $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
2068
                continue;
2069
            }
2070
            $prefix = '';
2071
            $secure = $this->SMTPSecure;
2072
            $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
2073
            if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
2074
                $prefix = 'ssl://';
2075
                $tls = false; //Can't have SSL and TLS at the same time
2076
                $secure = static::ENCRYPTION_SMTPS;
2077
            } elseif ('tls' === $hostinfo[1]) {
2078
                $tls = true;
2079
                //TLS doesn't use a prefix
2080
                $secure = static::ENCRYPTION_STARTTLS;
2081
            }
2082
            //Do we need the OpenSSL extension?
2083
            $sslext = defined('OPENSSL_ALGO_SHA256');
2084
            if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
2085
                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
2086
                if (!$sslext) {
2087
                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
2088
                }
2089
            }
2090
            $host = $hostinfo[2];
2091
            $port = $this->Port;
2092
            if (
2093
                array_key_exists(3, $hostinfo) &&
2094
                is_numeric($hostinfo[3]) &&
2095
                $hostinfo[3] > 0 &&
2096
                $hostinfo[3] < 65536
2097
            ) {
2098
                $port = (int) $hostinfo[3];
2099
            }
2100
            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
2101
                try {
2102
                    if ($this->Helo) {
2103
                        $hello = $this->Helo;
2104
                    } else {
2105
                        $hello = $this->serverHostname();
2106
                    }
2107
                    $this->smtp->hello($hello);
2108
                    //Automatically enable TLS encryption if:
2109
                    //* it's not disabled
2110
                    //* we have openssl extension
2111
                    //* we are not already using SSL
2112
                    //* the server offers STARTTLS
2113
                    if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) {
2114
                        $tls = true;
2115
                    }
2116
                    if ($tls) {
2117
                        if (!$this->smtp->startTLS()) {
2118
                            throw new Exception($this->lang('connect_host'));
2119
                        }
2120
                        //We must resend EHLO after TLS negotiation
2121
                        $this->smtp->hello($hello);
2122
                    }
2123
                    if (
2124
                        $this->SMTPAuth && !$this->smtp->authenticate(
2125
                            $this->Username,
2126
                            $this->Password,
2127
                            $this->AuthType,
2128
                            $this->oauth
2129
                        )
2130
                    ) {
2131
                        throw new Exception($this->lang('authenticate'));
2132
                    }
2133
2134
                    return true;
2135
                } catch (Exception $exc) {
2136
                    $lastexception = $exc;
2137
                    $this->edebug($exc->getMessage());
2138
                    //We must have connected, but then failed TLS or Auth, so close connection nicely
2139
                    $this->smtp->quit();
2140
                }
2141
            }
2142
        }
2143
        //If we get here, all connection attempts have failed, so close connection hard
2144
        $this->smtp->close();
2145
        //As we've caught all exceptions, just report whatever the last one was
2146
        if ($this->exceptions && null !== $lastexception) {
2147
            throw $lastexception;
2148
        }
2149
2150
        return false;
2151
    }
2152
2153
    /**
2154
     * Close the active SMTP session if one exists.
2155
     */
2156
    public function smtpClose()
2157
    {
2158
        if ((null !== $this->smtp) && $this->smtp->connected()) {
2159
            $this->smtp->quit();
2160
            $this->smtp->close();
2161
        }
2162
    }
2163
2164
    /**
2165
     * Set the language for error messages.
2166
     * Returns false if it cannot load the language file.
2167
     * The default language is English.
2168
     *
2169
     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
2170
     * @param string $lang_path Path to the language file directory, with trailing separator (slash)
2171
     *
2172
     * @return bool
2173
     */
2174
    public function setLanguage($langcode = 'en', $lang_path = '')
2175
    {
2176
        //Backwards compatibility for renamed language codes
2177
        $renamed_langcodes = [
2178
            'br' => 'pt_br',
2179
            'cz' => 'cs',
2180
            'dk' => 'da',
2181
            'no' => 'nb',
2182
            'se' => 'sv',
2183
            'rs' => 'sr',
2184
            'tg' => 'tl',
2185
            'am' => 'hy',
2186
        ];
2187
2188
        if (array_key_exists($langcode, $renamed_langcodes)) {
2189
            $langcode = $renamed_langcodes[$langcode];
2190
        }
2191
2192
        //Define full set of translatable strings in English
2193
        $PHPMAILER_LANG = [
2194
            'authenticate' => 'SMTP Error: Could not authenticate.',
2195
            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
2196
            'data_not_accepted' => 'SMTP Error: data not accepted.',
2197
            'empty_message' => 'Message body empty',
2198
            'encoding' => 'Unknown encoding: ',
2199
            'execute' => 'Could not execute: ',
2200
            'file_access' => 'Could not access file: ',
2201
            'file_open' => 'File Error: Could not open file: ',
2202
            'from_failed' => 'The following From address failed: ',
2203
            'instantiate' => 'Could not instantiate mail function.',
2204
            'invalid_address' => 'Invalid address: ',
2205
            'invalid_hostentry' => 'Invalid hostentry: ',
2206
            'invalid_host' => 'Invalid host: ',
2207
            'mailer_not_supported' => ' mailer is not supported.',
2208
            'provide_address' => 'You must provide at least one recipient email address.',
2209
            'recipients_failed' => 'SMTP Error: The following recipients failed: ',
2210
            'signing' => 'Signing Error: ',
2211
            'smtp_connect_failed' => 'SMTP connect() failed.',
2212
            'smtp_error' => 'SMTP server error: ',
2213
            'variable_set' => 'Cannot set or reset variable: ',
2214
            'extension_missing' => 'Extension missing: ',
2215
        ];
2216
        if (empty($lang_path)) {
2217
            //Calculate an absolute path so it can work if CWD is not here
2218
            $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
2219
        }
2220
        //Validate $langcode
2221
        if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) {
2222
            $langcode = 'en';
2223
        }
2224
        $foundlang = true;
2225
        $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
2226
        //There is no English translation file
2227
        if ('en' !== $langcode) {
2228
            //Make sure language file path is readable
2229
            if (!static::fileIsAccessible($lang_file)) {
2230
                $foundlang = false;
2231
            } else {
2232
                //Overwrite language-specific strings.
2233
                //This way we'll never have missing translation keys.
2234
                $foundlang = include $lang_file;
2235
            }
2236
        }
2237
        $this->language = $PHPMAILER_LANG;
2238
2239
        return (bool) $foundlang; //Returns false if language not found
2240
    }
2241
2242
    /**
2243
     * Get the array of strings for the current language.
2244
     *
2245
     * @return array
2246
     */
2247
    public function getTranslations()
2248
    {
2249
        return $this->language;
2250
    }
2251
2252
    /**
2253
     * Create recipient headers.
2254
     *
2255
     * @param string $type
2256
     * @param array  $addr An array of recipients,
2257
     *                     where each recipient is a 2-element indexed array with element 0 containing an address
2258
     *                     and element 1 containing a name, like:
2259
     *                     [['[email protected]', 'Joe User'], ['[email protected]', 'Zoe User']]
2260
     *
2261
     * @return string
2262
     */
2263
    public function addrAppend($type, $addr)
2264
    {
2265
        $addresses = [];
2266
        foreach ($addr as $address) {
2267
            $addresses[] = $this->addrFormat($address);
2268
        }
2269
2270
        return $type . ': ' . implode(', ', $addresses) . static::$LE;
2271
    }
2272
2273
    /**
2274
     * Format an address for use in a message header.
2275
     *
2276
     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
2277
     *                    ['[email protected]', 'Joe User']
2278
     *
2279
     * @return string
2280
     */
2281
    public function addrFormat($addr)
2282
    {
2283
        if (empty($addr[1])) { //No name provided
2284
            return $this->secureHeader($addr[0]);
2285
        }
2286
2287
        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
2288
            ' <' . $this->secureHeader($addr[0]) . '>';
2289
    }
2290
2291
    /**
2292
     * Word-wrap message.
2293
     * For use with mailers that do not automatically perform wrapping
2294
     * and for quoted-printable encoded messages.
2295
     * Original written by philippe.
2296
     *
2297
     * @param string $message The message to wrap
2298
     * @param int    $length  The line length to wrap to
2299
     * @param bool   $qp_mode Whether to run in Quoted-Printable mode
2300
     *
2301
     * @return string
2302
     */
2303
    public function wrapText($message, $length, $qp_mode = false)
2304
    {
2305
        if ($qp_mode) {
2306
            $soft_break = sprintf(' =%s', static::$LE);
2307
        } else {
2308
            $soft_break = static::$LE;
2309
        }
2310
        //If utf-8 encoding is used, we will need to make sure we don't
2311
        //split multibyte characters when we wrap
2312
        $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
2313
        $lelen = strlen(static::$LE);
2314
        $crlflen = strlen(static::$LE);
2315
2316
        $message = static::normalizeBreaks($message);
2317
        //Remove a trailing line break
2318
        if (substr($message, -$lelen) === static::$LE) {
2319
            $message = substr($message, 0, -$lelen);
2320
        }
2321
2322
        //Split message into lines
2323
        $lines = explode(static::$LE, $message);
2324
        //Message will be rebuilt in here
2325
        $message = '';
2326
        foreach ($lines as $line) {
2327
            $words = explode(' ', $line);
2328
            $buf = '';
2329
            $firstword = true;
2330
            foreach ($words as $word) {
2331
                if ($qp_mode && (strlen($word) > $length)) {
2332
                    $space_left = $length - strlen($buf) - $crlflen;
2333
                    if (!$firstword) {
2334
                        if ($space_left > 20) {
2335
                            $len = $space_left;
2336
                            if ($is_utf8) {
2337
                                $len = $this->utf8CharBoundary($word, $len);
2338
                            } elseif ('=' === substr($word, $len - 1, 1)) {
2339
                                --$len;
2340
                            } elseif ('=' === substr($word, $len - 2, 1)) {
2341
                                $len -= 2;
2342
                            }
2343
                            $part = substr($word, 0, $len);
2344
                            $word = substr($word, $len);
2345
                            $buf .= ' ' . $part;
2346
                            $message .= $buf . sprintf('=%s', static::$LE);
2347
                        } else {
2348
                            $message .= $buf . $soft_break;
2349
                        }
2350
                        $buf = '';
2351
                    }
2352
                    while ($word !== '') {
2353
                        if ($length <= 0) {
2354
                            break;
2355
                        }
2356
                        $len = $length;
2357
                        if ($is_utf8) {
2358
                            $len = $this->utf8CharBoundary($word, $len);
2359
                        } elseif ('=' === substr($word, $len - 1, 1)) {
2360
                            --$len;
2361
                        } elseif ('=' === substr($word, $len - 2, 1)) {
2362
                            $len -= 2;
2363
                        }
2364
                        $part = substr($word, 0, $len);
2365
                        $word = (string) substr($word, $len);
2366
2367
                        if ($word !== '') {
2368
                            $message .= $part . sprintf('=%s', static::$LE);
2369
                        } else {
2370
                            $buf = $part;
2371
                        }
2372
                    }
2373
                } else {
2374
                    $buf_o = $buf;
2375
                    if (!$firstword) {
2376
                        $buf .= ' ';
2377
                    }
2378
                    $buf .= $word;
2379
2380
                    if ('' !== $buf_o && strlen($buf) > $length) {
2381
                        $message .= $buf_o . $soft_break;
2382
                        $buf = $word;
2383
                    }
2384
                }
2385
                $firstword = false;
2386
            }
2387
            $message .= $buf . static::$LE;
2388
        }
2389
2390
        return $message;
2391
    }
2392
2393
    /**
2394
     * Find the last character boundary prior to $maxLength in a utf-8
2395
     * quoted-printable encoded string.
2396
     * Original written by Colin Brown.
2397
     *
2398
     * @param string $encodedText utf-8 QP text
2399
     * @param int    $maxLength   Find the last character boundary prior to this length
2400
     *
2401
     * @return int
2402
     */
2403
    public function utf8CharBoundary($encodedText, $maxLength)
2404
    {
2405
        $foundSplitPos = false;
2406
        $lookBack = 3;
2407
        while (!$foundSplitPos) {
2408
            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
2409
            $encodedCharPos = strpos($lastChunk, '=');
2410
            if (false !== $encodedCharPos) {
2411
                //Found start of encoded character byte within $lookBack block.
2412
                //Check the encoded byte value (the 2 chars after the '=')
2413
                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
2414
                $dec = hexdec($hex);
2415
                if ($dec < 128) {
2416
                    //Single byte character.
2417
                    //If the encoded char was found at pos 0, it will fit
2418
                    //otherwise reduce maxLength to start of the encoded char
2419
                    if ($encodedCharPos > 0) {
2420
                        $maxLength -= $lookBack - $encodedCharPos;
2421
                    }
2422
                    $foundSplitPos = true;
2423
                } elseif ($dec >= 192) {
2424
                    //First byte of a multi byte character
2425
                    //Reduce maxLength to split at start of character
2426
                    $maxLength -= $lookBack - $encodedCharPos;
2427
                    $foundSplitPos = true;
2428
                } elseif ($dec < 192) {
2429
                    //Middle byte of a multi byte character, look further back
2430
                    $lookBack += 3;
2431
                }
2432
            } else {
2433
                //No encoded character found
2434
                $foundSplitPos = true;
2435
            }
2436
        }
2437
2438
        return $maxLength;
2439
    }
2440
2441
    /**
2442
     * Apply word wrapping to the message body.
2443
     * Wraps the message body to the number of chars set in the WordWrap property.
2444
     * You should only do this to plain-text bodies as wrapping HTML tags may break them.
2445
     * This is called automatically by createBody(), so you don't need to call it yourself.
2446
     */
2447
    public function setWordWrap()
2448
    {
2449
        if ($this->WordWrap < 1) {
2450
            return;
2451
        }
2452
2453
        switch ($this->message_type) {
2454
            case 'alt':
2455
            case 'alt_inline':
2456
            case 'alt_attach':
2457
            case 'alt_inline_attach':
2458
                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
2459
                break;
2460
            default:
2461
                $this->Body = $this->wrapText($this->Body, $this->WordWrap);
2462
                break;
2463
        }
2464
    }
2465
2466
    /**
2467
     * Assemble message headers.
2468
     *
2469
     * @return string The assembled headers
2470
     */
2471
    public function createHeader()
2472
    {
2473
        $result = '';
2474
2475
        $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
2476
2477
        //The To header is created automatically by mail(), so needs to be omitted here
2478
        if ('mail' !== $this->Mailer) {
2479
            if ($this->SingleTo) {
0 ignored issues
show
Deprecated Code introduced by
The property PHPMailer\PHPMailer\PHPMailer::$SingleTo has been deprecated: 6.0.0 PHPMailer isn't a mailing list manager! ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2479
            if (/** @scrutinizer ignore-deprecated */ $this->SingleTo) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
2480
                foreach ($this->to as $toaddr) {
2481
                    $this->SingleToArray[] = $this->addrFormat($toaddr);
2482
                }
2483
            } elseif (count($this->to) > 0) {
2484
                $result .= $this->addrAppend('To', $this->to);
2485
            } elseif (count($this->cc) === 0) {
2486
                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
2487
            }
2488
        }
2489
        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
2490
2491
        //sendmail and mail() extract Cc from the header before sending
2492
        if (count($this->cc) > 0) {
2493
            $result .= $this->addrAppend('Cc', $this->cc);
2494
        }
2495
2496
        //sendmail and mail() extract Bcc from the header before sending
2497
        if (
2498
            (
2499
                'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
2500
            )
2501
            && count($this->bcc) > 0
2502
        ) {
2503
            $result .= $this->addrAppend('Bcc', $this->bcc);
2504
        }
2505
2506
        if (count($this->ReplyTo) > 0) {
2507
            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
2508
        }
2509
2510
        //mail() sets the subject itself
2511
        if ('mail' !== $this->Mailer) {
2512
            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
2513
        }
2514
2515
        //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
2516
        //https://tools.ietf.org/html/rfc5322#section-3.6.4
2517
        if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageID)) {
2518
            $this->lastMessageID = $this->MessageID;
2519
        } else {
2520
            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
2521
        }
2522
        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
2523
        if (null !== $this->Priority) {
2524
            $result .= $this->headerLine('X-Priority', $this->Priority);
2525
        }
2526
        if ('' === $this->XMailer) {
2527
            $result .= $this->headerLine(
2528
                'X-Mailer',
2529
                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
2530
            );
2531
        } else {
2532
            $myXmailer = trim($this->XMailer);
0 ignored issues
show
Bug introduced by
It seems like $this->XMailer can also be of type null; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2532
            $myXmailer = trim(/** @scrutinizer ignore-type */ $this->XMailer);
Loading history...
2533
            if ($myXmailer) {
2534
                $result .= $this->headerLine('X-Mailer', $myXmailer);
2535
            }
2536
        }
2537
2538
        if ('' !== $this->ConfirmReadingTo) {
2539
            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
2540
        }
2541
2542
        //Add custom headers
2543
        foreach ($this->CustomHeader as $header) {
2544
            $result .= $this->headerLine(
2545
                trim($header[0]),
2546
                $this->encodeHeader(trim($header[1]))
2547
            );
2548
        }
2549
        if (!$this->sign_key_file) {
2550
            $result .= $this->headerLine('MIME-Version', '1.0');
2551
            $result .= $this->getMailMIME();
2552
        }
2553
2554
        return $result;
2555
    }
2556
2557
    /**
2558
     * Get the message MIME type headers.
2559
     *
2560
     * @return string
2561
     */
2562
    public function getMailMIME()
2563
    {
2564
        $result = '';
2565
        $ismultipart = true;
2566
        switch ($this->message_type) {
2567
            case 'inline':
2568
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2569
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2570
                break;
2571
            case 'attach':
2572
            case 'inline_attach':
2573
            case 'alt_attach':
2574
            case 'alt_inline_attach':
2575
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';');
2576
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2577
                break;
2578
            case 'alt':
2579
            case 'alt_inline':
2580
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2581
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2582
                break;
2583
            default:
2584
                //Catches case 'plain': and case '':
2585
                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
2586
                $ismultipart = false;
2587
                break;
2588
        }
2589
        //RFC1341 part 5 says 7bit is assumed if not specified
2590
        if (static::ENCODING_7BIT !== $this->Encoding) {
2591
            //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
2592
            if ($ismultipart) {
2593
                if (static::ENCODING_8BIT === $this->Encoding) {
2594
                    $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
2595
                }
2596
                //The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
2597
            } else {
2598
                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
2599
            }
2600
        }
2601
2602
        return $result;
2603
    }
2604
2605
    /**
2606
     * Returns the whole MIME message.
2607
     * Includes complete headers and body.
2608
     * Only valid post preSend().
2609
     *
2610
     * @see PHPMailer::preSend()
2611
     *
2612
     * @return string
2613
     */
2614
    public function getSentMIMEMessage()
2615
    {
2616
        return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) .
2617
            static::$LE . static::$LE . $this->MIMEBody;
2618
    }
2619
2620
    /**
2621
     * Create a unique ID to use for boundaries.
2622
     *
2623
     * @return string
2624
     */
2625
    protected function generateId()
2626
    {
2627
        $len = 32; //32 bytes = 256 bits
2628
        $bytes = '';
2629
        if (function_exists('random_bytes')) {
2630
            try {
2631
                $bytes = random_bytes($len);
2632
            } catch (\Exception $e) {
2633
                //Do nothing
2634
            }
2635
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
2636
            /** @noinspection CryptographicallySecureRandomnessInspection */
2637
            $bytes = openssl_random_pseudo_bytes($len);
2638
        }
2639
        if ($bytes === '') {
2640
            //We failed to produce a proper random string, so make do.
2641
            //Use a hash to force the length to the same as the other methods
2642
            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
2643
        }
2644
2645
        //We don't care about messing up base64 format here, just want a random string
2646
        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
2647
    }
2648
2649
    /**
2650
     * Assemble the message body.
2651
     * Returns an empty string on failure.
2652
     *
2653
     * @throws Exception
2654
     *
2655
     * @return string The assembled message body
2656
     */
2657
    public function createBody()
2658
    {
2659
        $body = '';
2660
        //Create unique IDs and preset boundaries
2661
        $this->uniqueid = $this->generateId();
2662
        $this->boundary[1] = 'b1_' . $this->uniqueid;
2663
        $this->boundary[2] = 'b2_' . $this->uniqueid;
2664
        $this->boundary[3] = 'b3_' . $this->uniqueid;
2665
2666
        if ($this->sign_key_file) {
2667
            $body .= $this->getMailMIME() . static::$LE;
2668
        }
2669
2670
        $this->setWordWrap();
2671
2672
        $bodyEncoding = $this->Encoding;
2673
        $bodyCharSet = $this->CharSet;
2674
        //Can we do a 7-bit downgrade?
2675
        if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {
2676
            $bodyEncoding = static::ENCODING_7BIT;
2677
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2678
            $bodyCharSet = static::CHARSET_ASCII;
2679
        }
2680
        //If lines are too long, and we're not already using an encoding that will shorten them,
2681
        //change to quoted-printable transfer encoding for the body part only
2682
        if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {
2683
            $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2684
        }
2685
2686
        $altBodyEncoding = $this->Encoding;
2687
        $altBodyCharSet = $this->CharSet;
2688
        //Can we do a 7-bit downgrade?
2689
        if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {
2690
            $altBodyEncoding = static::ENCODING_7BIT;
2691
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2692
            $altBodyCharSet = static::CHARSET_ASCII;
2693
        }
2694
        //If lines are too long, and we're not already using an encoding that will shorten them,
2695
        //change to quoted-printable transfer encoding for the alt body part only
2696
        if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {
2697
            $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2698
        }
2699
        //Use this as a preamble in all multipart message types
2700
        $mimepre = 'This is a multi-part message in MIME format.' . static::$LE . static::$LE;
2701
        switch ($this->message_type) {
2702
            case 'inline':
2703
                $body .= $mimepre;
2704
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2705
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2706
                $body .= static::$LE;
2707
                $body .= $this->attachAll('inline', $this->boundary[1]);
2708
                break;
2709
            case 'attach':
2710
                $body .= $mimepre;
2711
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2712
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2713
                $body .= static::$LE;
2714
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2715
                break;
2716
            case 'inline_attach':
2717
                $body .= $mimepre;
2718
                $body .= $this->textLine('--' . $this->boundary[1]);
2719
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2720
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
2721
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2722
                $body .= static::$LE;
2723
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
2724
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2725
                $body .= static::$LE;
2726
                $body .= $this->attachAll('inline', $this->boundary[2]);
2727
                $body .= static::$LE;
2728
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2729
                break;
2730
            case 'alt':
2731
                $body .= $mimepre;
2732
                $body .= $this->getBoundary(
2733
                    $this->boundary[1],
2734
                    $altBodyCharSet,
2735
                    static::CONTENT_TYPE_PLAINTEXT,
2736
                    $altBodyEncoding
2737
                );
2738
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2739
                $body .= static::$LE;
2740
                $body .= $this->getBoundary(
2741
                    $this->boundary[1],
2742
                    $bodyCharSet,
2743
                    static::CONTENT_TYPE_TEXT_HTML,
2744
                    $bodyEncoding
2745
                );
2746
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2747
                $body .= static::$LE;
2748
                if (!empty($this->Ical)) {
2749
                    $method = static::ICAL_METHOD_REQUEST;
2750
                    foreach (static::$IcalMethods as $imethod) {
2751
                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
2752
                            $method = $imethod;
2753
                            break;
2754
                        }
2755
                    }
2756
                    $body .= $this->getBoundary(
2757
                        $this->boundary[1],
2758
                        '',
2759
                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
2760
                        ''
2761
                    );
2762
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2763
                    $body .= static::$LE;
2764
                }
2765
                $body .= $this->endBoundary($this->boundary[1]);
2766
                break;
2767
            case 'alt_inline':
2768
                $body .= $mimepre;
2769
                $body .= $this->getBoundary(
2770
                    $this->boundary[1],
2771
                    $altBodyCharSet,
2772
                    static::CONTENT_TYPE_PLAINTEXT,
2773
                    $altBodyEncoding
2774
                );
2775
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2776
                $body .= static::$LE;
2777
                $body .= $this->textLine('--' . $this->boundary[1]);
2778
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2779
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
2780
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2781
                $body .= static::$LE;
2782
                $body .= $this->getBoundary(
2783
                    $this->boundary[2],
2784
                    $bodyCharSet,
2785
                    static::CONTENT_TYPE_TEXT_HTML,
2786
                    $bodyEncoding
2787
                );
2788
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2789
                $body .= static::$LE;
2790
                $body .= $this->attachAll('inline', $this->boundary[2]);
2791
                $body .= static::$LE;
2792
                $body .= $this->endBoundary($this->boundary[1]);
2793
                break;
2794
            case 'alt_attach':
2795
                $body .= $mimepre;
2796
                $body .= $this->textLine('--' . $this->boundary[1]);
2797
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2798
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
2799
                $body .= static::$LE;
2800
                $body .= $this->getBoundary(
2801
                    $this->boundary[2],
2802
                    $altBodyCharSet,
2803
                    static::CONTENT_TYPE_PLAINTEXT,
2804
                    $altBodyEncoding
2805
                );
2806
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2807
                $body .= static::$LE;
2808
                $body .= $this->getBoundary(
2809
                    $this->boundary[2],
2810
                    $bodyCharSet,
2811
                    static::CONTENT_TYPE_TEXT_HTML,
2812
                    $bodyEncoding
2813
                );
2814
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2815
                $body .= static::$LE;
2816
                if (!empty($this->Ical)) {
2817
                    $method = static::ICAL_METHOD_REQUEST;
2818
                    foreach (static::$IcalMethods as $imethod) {
2819
                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
2820
                            $method = $imethod;
2821
                            break;
2822
                        }
2823
                    }
2824
                    $body .= $this->getBoundary(
2825
                        $this->boundary[2],
2826
                        '',
2827
                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
2828
                        ''
2829
                    );
2830
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2831
                }
2832
                $body .= $this->endBoundary($this->boundary[2]);
2833
                $body .= static::$LE;
2834
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2835
                break;
2836
            case 'alt_inline_attach':
2837
                $body .= $mimepre;
2838
                $body .= $this->textLine('--' . $this->boundary[1]);
2839
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2840
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
2841
                $body .= static::$LE;
2842
                $body .= $this->getBoundary(
2843
                    $this->boundary[2],
2844
                    $altBodyCharSet,
2845
                    static::CONTENT_TYPE_PLAINTEXT,
2846
                    $altBodyEncoding
2847
                );
2848
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2849
                $body .= static::$LE;
2850
                $body .= $this->textLine('--' . $this->boundary[2]);
2851
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2852
                $body .= $this->textLine(' boundary="' . $this->boundary[3] . '";');
2853
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2854
                $body .= static::$LE;
2855
                $body .= $this->getBoundary(
2856
                    $this->boundary[3],
2857
                    $bodyCharSet,
2858
                    static::CONTENT_TYPE_TEXT_HTML,
2859
                    $bodyEncoding
2860
                );
2861
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2862
                $body .= static::$LE;
2863
                $body .= $this->attachAll('inline', $this->boundary[3]);
2864
                $body .= static::$LE;
2865
                $body .= $this->endBoundary($this->boundary[2]);
2866
                $body .= static::$LE;
2867
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2868
                break;
2869
            default:
2870
                //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
2871
                //Reset the `Encoding` property in case we changed it for line length reasons
2872
                $this->Encoding = $bodyEncoding;
2873
                $body .= $this->encodeString($this->Body, $this->Encoding);
2874
                break;
2875
        }
2876
2877
        if ($this->isError()) {
2878
            $body = '';
2879
            if ($this->exceptions) {
2880
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
2881
            }
2882
        } elseif ($this->sign_key_file) {
2883
            try {
2884
                if (!defined('PKCS7_TEXT')) {
2885
                    throw new Exception($this->lang('extension_missing') . 'openssl');
2886
                }
2887
2888
                $file = tempnam(sys_get_temp_dir(), 'srcsign');
2889
                $signed = tempnam(sys_get_temp_dir(), 'mailsign');
2890
                file_put_contents($file, $body);
2891
2892
                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
2893
                if (empty($this->sign_extracerts_file)) {
2894
                    $sign = @openssl_pkcs7_sign(
2895
                        $file,
2896
                        $signed,
2897
                        'file://' . realpath($this->sign_cert_file),
2898
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2899
                        []
2900
                    );
2901
                } else {
2902
                    $sign = @openssl_pkcs7_sign(
2903
                        $file,
2904
                        $signed,
2905
                        'file://' . realpath($this->sign_cert_file),
2906
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2907
                        [],
2908
                        PKCS7_DETACHED,
2909
                        $this->sign_extracerts_file
2910
                    );
2911
                }
2912
2913
                @unlink($file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

2913
                /** @scrutinizer ignore-unhandled */ @unlink($file);

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2914
                if ($sign) {
2915
                    $body = file_get_contents($signed);
2916
                    @unlink($signed);
2917
                    //The message returned by openssl contains both headers and body, so need to split them up
2918
                    $parts = explode("\n\n", $body, 2);
2919
                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
2920
                    $body = $parts[1];
2921
                } else {
2922
                    @unlink($signed);
2923
                    throw new Exception($this->lang('signing') . openssl_error_string());
2924
                }
2925
            } catch (Exception $exc) {
2926
                $body = '';
2927
                if ($this->exceptions) {
2928
                    throw $exc;
2929
                }
2930
            }
2931
        }
2932
2933
        return $body;
2934
    }
2935
2936
    /**
2937
     * Return the start of a message boundary.
2938
     *
2939
     * @param string $boundary
2940
     * @param string $charSet
2941
     * @param string $contentType
2942
     * @param string $encoding
2943
     *
2944
     * @return string
2945
     */
2946
    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
2947
    {
2948
        $result = '';
2949
        if ('' === $charSet) {
2950
            $charSet = $this->CharSet;
2951
        }
2952
        if ('' === $contentType) {
2953
            $contentType = $this->ContentType;
2954
        }
2955
        if ('' === $encoding) {
2956
            $encoding = $this->Encoding;
2957
        }
2958
        $result .= $this->textLine('--' . $boundary);
2959
        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
2960
        $result .= static::$LE;
2961
        //RFC1341 part 5 says 7bit is assumed if not specified
2962
        if (static::ENCODING_7BIT !== $encoding) {
2963
            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
2964
        }
2965
        $result .= static::$LE;
2966
2967
        return $result;
2968
    }
2969
2970
    /**
2971
     * Return the end of a message boundary.
2972
     *
2973
     * @param string $boundary
2974
     *
2975
     * @return string
2976
     */
2977
    protected function endBoundary($boundary)
2978
    {
2979
        return static::$LE . '--' . $boundary . '--' . static::$LE;
2980
    }
2981
2982
    /**
2983
     * Set the message type.
2984
     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
2985
     */
2986
    protected function setMessageType()
2987
    {
2988
        $type = [];
2989
        if ($this->alternativeExists()) {
2990
            $type[] = 'alt';
2991
        }
2992
        if ($this->inlineImageExists()) {
2993
            $type[] = 'inline';
2994
        }
2995
        if ($this->attachmentExists()) {
2996
            $type[] = 'attach';
2997
        }
2998
        $this->message_type = implode('_', $type);
2999
        if ('' === $this->message_type) {
3000
            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
3001
            $this->message_type = 'plain';
3002
        }
3003
    }
3004
3005
    /**
3006
     * Format a header line.
3007
     *
3008
     * @param string     $name
3009
     * @param string|int $value
3010
     *
3011
     * @return string
3012
     */
3013
    public function headerLine($name, $value)
3014
    {
3015
        return $name . ': ' . $value . static::$LE;
3016
    }
3017
3018
    /**
3019
     * Return a formatted mail line.
3020
     *
3021
     * @param string $value
3022
     *
3023
     * @return string
3024
     */
3025
    public function textLine($value)
3026
    {
3027
        return $value . static::$LE;
3028
    }
3029
3030
    /**
3031
     * Add an attachment from a path on the filesystem.
3032
     * Never use a user-supplied path to a file!
3033
     * Returns false if the file could not be found or read.
3034
     * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.
3035
     * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
3036
     *
3037
     * @param string $path        Path to the attachment
3038
     * @param string $name        Overrides the attachment name
3039
     * @param string $encoding    File encoding (see $Encoding)
3040
     * @param string $type        MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
3041
     * @param string $disposition Disposition to use
3042
     *
3043
     * @throws Exception
3044
     *
3045
     * @return bool
3046
     */
3047
    public function addAttachment(
3048
        $path,
3049
        $name = '',
3050
        $encoding = self::ENCODING_BASE64,
3051
        $type = '',
3052
        $disposition = 'attachment'
3053
    ) {
3054
        try {
3055
            if (!static::fileIsAccessible($path)) {
3056
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
3057
            }
3058
3059
            //If a MIME type is not specified, try to work it out from the file name
3060
            if ('' === $type) {
3061
                $type = static::filenameToType($path);
3062
            }
3063
3064
            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
3065
            if ('' === $name) {
3066
                $name = $filename;
3067
            }
3068
            if (!$this->validateEncoding($encoding)) {
3069
                throw new Exception($this->lang('encoding') . $encoding);
3070
            }
3071
3072
            $this->attachment[] = [
3073
                0 => $path,
3074
                1 => $filename,
3075
                2 => $name,
3076
                3 => $encoding,
3077
                4 => $type,
3078
                5 => false, //isStringAttachment
3079
                6 => $disposition,
3080
                7 => $name,
3081
            ];
3082
        } catch (Exception $exc) {
3083
            $this->setError($exc->getMessage());
3084
            $this->edebug($exc->getMessage());
3085
            if ($this->exceptions) {
3086
                throw $exc;
3087
            }
3088
3089
            return false;
3090
        }
3091
3092
        return true;
3093
    }
3094
3095
    /**
3096
     * Return the array of attachments.
3097
     *
3098
     * @return array
3099
     */
3100
    public function getAttachments()
3101
    {
3102
        return $this->attachment;
3103
    }
3104
3105
    /**
3106
     * Attach all file, string, and binary attachments to the message.
3107
     * Returns an empty string on failure.
3108
     *
3109
     * @param string $disposition_type
3110
     * @param string $boundary
3111
     *
3112
     * @throws Exception
3113
     *
3114
     * @return string
3115
     */
3116
    protected function attachAll($disposition_type, $boundary)
3117
    {
3118
        //Return text of body
3119
        $mime = [];
3120
        $cidUniq = [];
3121
        $incl = [];
3122
3123
        //Add all attachments
3124
        foreach ($this->attachment as $attachment) {
3125
            //Check if it is a valid disposition_filter
3126
            if ($attachment[6] === $disposition_type) {
3127
                //Check for string attachment
3128
                $string = '';
3129
                $path = '';
3130
                $bString = $attachment[5];
3131
                if ($bString) {
3132
                    $string = $attachment[0];
3133
                } else {
3134
                    $path = $attachment[0];
3135
                }
3136
3137
                $inclhash = hash('sha256', serialize($attachment));
3138
                if (in_array($inclhash, $incl, true)) {
3139
                    continue;
3140
                }
3141
                $incl[] = $inclhash;
3142
                $name = $attachment[2];
3143
                $encoding = $attachment[3];
3144
                $type = $attachment[4];
3145
                $disposition = $attachment[6];
3146
                $cid = $attachment[7];
3147
                if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {
3148
                    continue;
3149
                }
3150
                $cidUniq[$cid] = true;
3151
3152
                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
3153
                //Only include a filename property if we have one
3154
                if (!empty($name)) {
3155
                    $mime[] = sprintf(
3156
                        'Content-Type: %s; name=%s%s',
3157
                        $type,
3158
                        static::quotedString($this->encodeHeader($this->secureHeader($name))),
3159
                        static::$LE
3160
                    );
3161
                } else {
3162
                    $mime[] = sprintf(
3163
                        'Content-Type: %s%s',
3164
                        $type,
3165
                        static::$LE
3166
                    );
3167
                }
3168
                //RFC1341 part 5 says 7bit is assumed if not specified
3169
                if (static::ENCODING_7BIT !== $encoding) {
3170
                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
3171
                }
3172
3173
                //Only set Content-IDs on inline attachments
3174
                if ((string) $cid !== '' && $disposition === 'inline') {
3175
                    $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
3176
                }
3177
3178
                //Allow for bypassing the Content-Disposition header
3179
                if (!empty($disposition)) {
3180
                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
3181
                    if (!empty($encoded_name)) {
3182
                        $mime[] = sprintf(
3183
                            'Content-Disposition: %s; filename=%s%s',
3184
                            $disposition,
3185
                            static::quotedString($encoded_name),
3186
                            static::$LE . static::$LE
3187
                        );
3188
                    } else {
3189
                        $mime[] = sprintf(
3190
                            'Content-Disposition: %s%s',
3191
                            $disposition,
3192
                            static::$LE . static::$LE
3193
                        );
3194
                    }
3195
                } else {
3196
                    $mime[] = static::$LE;
3197
                }
3198
3199
                //Encode as string attachment
3200
                if ($bString) {
3201
                    $mime[] = $this->encodeString($string, $encoding);
3202
                } else {
3203
                    $mime[] = $this->encodeFile($path, $encoding);
3204
                }
3205
                if ($this->isError()) {
3206
                    return '';
3207
                }
3208
                $mime[] = static::$LE;
3209
            }
3210
        }
3211
3212
        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
3213
3214
        return implode('', $mime);
3215
    }
3216
3217
    /**
3218
     * Encode a file attachment in requested format.
3219
     * Returns an empty string on failure.
3220
     *
3221
     * @param string $path     The full path to the file
3222
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3223
     *
3224
     * @return string
3225
     */
3226
    protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
3227
    {
3228
        try {
3229
            if (!static::fileIsAccessible($path)) {
3230
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
3231
            }
3232
            $file_buffer = file_get_contents($path);
3233
            if (false === $file_buffer) {
3234
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
3235
            }
3236
            $file_buffer = $this->encodeString($file_buffer, $encoding);
3237
3238
            return $file_buffer;
3239
        } catch (Exception $exc) {
3240
            $this->setError($exc->getMessage());
3241
            $this->edebug($exc->getMessage());
3242
            if ($this->exceptions) {
3243
                throw $exc;
3244
            }
3245
3246
            return '';
3247
        }
3248
    }
3249
3250
    /**
3251
     * Encode a string in requested format.
3252
     * Returns an empty string on failure.
3253
     *
3254
     * @param string $str      The text to encode
3255
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3256
     *
3257
     * @throws Exception
3258
     *
3259
     * @return string
3260
     */
3261
    public function encodeString($str, $encoding = self::ENCODING_BASE64)
3262
    {
3263
        $encoded = '';
3264
        switch (strtolower($encoding)) {
3265
            case static::ENCODING_BASE64:
3266
                $encoded = chunk_split(
3267
                    base64_encode($str),
3268
                    static::STD_LINE_LENGTH,
3269
                    static::$LE
3270
                );
3271
                break;
3272
            case static::ENCODING_7BIT:
3273
            case static::ENCODING_8BIT:
3274
                $encoded = static::normalizeBreaks($str);
3275
                //Make sure it ends with a line break
3276
                if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
3277
                    $encoded .= static::$LE;
3278
                }
3279
                break;
3280
            case static::ENCODING_BINARY:
3281
                $encoded = $str;
3282
                break;
3283
            case static::ENCODING_QUOTED_PRINTABLE:
3284
                $encoded = $this->encodeQP($str);
3285
                break;
3286
            default:
3287
                $this->setError($this->lang('encoding') . $encoding);
3288
                if ($this->exceptions) {
3289
                    throw new Exception($this->lang('encoding') . $encoding);
3290
                }
3291
                break;
3292
        }
3293
3294
        return $encoded;
3295
    }
3296
3297
    /**
3298
     * Encode a header value (not including its label) optimally.
3299
     * Picks shortest of Q, B, or none. Result includes folding if needed.
3300
     * See RFC822 definitions for phrase, comment and text positions.
3301
     *
3302
     * @param string $str      The header value to encode
3303
     * @param string $position What context the string will be used in
3304
     *
3305
     * @return string
3306
     */
3307
    public function encodeHeader($str, $position = 'text')
3308
    {
3309
        $matchcount = 0;
3310
        switch (strtolower($position)) {
3311
            case 'phrase':
3312
                if (!preg_match('/[\200-\377]/', $str)) {
3313
                    //Can't use addslashes as we don't know the value of magic_quotes_sybase
3314
                    $encoded = addcslashes($str, "\0..\37\177\\\"");
3315
                    if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
3316
                        return $encoded;
3317
                    }
3318
3319
                    return "\"$encoded\"";
3320
                }
3321
                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
3322
                break;
3323
            /* @noinspection PhpMissingBreakStatementInspection */
3324
            case 'comment':
3325
                $matchcount = preg_match_all('/[()"]/', $str, $matches);
3326
            //fallthrough
3327
            case 'text':
3328
            default:
3329
                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
3330
                break;
3331
        }
3332
3333
        if ($this->has8bitChars($str)) {
3334
            $charset = $this->CharSet;
3335
        } else {
3336
            $charset = static::CHARSET_ASCII;
3337
        }
3338
3339
        //Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
3340
        $overhead = 8 + strlen($charset);
3341
3342
        if ('mail' === $this->Mailer) {
3343
            $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
3344
        } else {
3345
            $maxlen = static::MAX_LINE_LENGTH - $overhead;
3346
        }
3347
3348
        //Select the encoding that produces the shortest output and/or prevents corruption.
3349
        if ($matchcount > strlen($str) / 3) {
3350
            //More than 1/3 of the content needs encoding, use B-encode.
3351
            $encoding = 'B';
3352
        } elseif ($matchcount > 0) {
3353
            //Less than 1/3 of the content needs encoding, use Q-encode.
3354
            $encoding = 'Q';
3355
        } elseif (strlen($str) > $maxlen) {
3356
            //No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
3357
            $encoding = 'Q';
3358
        } else {
3359
            //No reformatting needed
3360
            $encoding = false;
3361
        }
3362
3363
        switch ($encoding) {
3364
            case 'B':
3365
                if ($this->hasMultiBytes($str)) {
3366
                    //Use a custom function which correctly encodes and wraps long
3367
                    //multibyte strings without breaking lines within a character
3368
                    $encoded = $this->base64EncodeWrapMB($str, "\n");
3369
                } else {
3370
                    $encoded = base64_encode($str);
3371
                    $maxlen -= $maxlen % 4;
3372
                    $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
3373
                }
3374
                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
3375
                break;
3376
            case 'Q':
3377
                $encoded = $this->encodeQ($str, $position);
3378
                $encoded = $this->wrapText($encoded, $maxlen, true);
3379
                $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
3380
                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
3381
                break;
3382
            default:
3383
                return $str;
3384
        }
3385
3386
        return trim(static::normalizeBreaks($encoded));
3387
    }
3388
3389
    /**
3390
     * Check if a string contains multi-byte characters.
3391
     *
3392
     * @param string $str multi-byte text to wrap encode
3393
     *
3394
     * @return bool
3395
     */
3396
    public function hasMultiBytes($str)
3397
    {
3398
        if (function_exists('mb_strlen')) {
3399
            return strlen($str) > mb_strlen($str, $this->CharSet);
3400
        }
3401
3402
        //Assume no multibytes (we can't handle without mbstring functions anyway)
3403
        return false;
3404
    }
3405
3406
    /**
3407
     * Does a string contain any 8-bit chars (in any charset)?
3408
     *
3409
     * @param string $text
3410
     *
3411
     * @return bool
3412
     */
3413
    public function has8bitChars($text)
3414
    {
3415
        return (bool) preg_match('/[\x80-\xFF]/', $text);
3416
    }
3417
3418
    /**
3419
     * Encode and wrap long multibyte strings for mail headers
3420
     * without breaking lines within a character.
3421
     * Adapted from a function by paravoid.
3422
     *
3423
     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
3424
     *
3425
     * @param string $str       multi-byte text to wrap encode
3426
     * @param string $linebreak string to use as linefeed/end-of-line
3427
     *
3428
     * @return string
3429
     */
3430
    public function base64EncodeWrapMB($str, $linebreak = null)
3431
    {
3432
        $start = '=?' . $this->CharSet . '?B?';
3433
        $end = '?=';
3434
        $encoded = '';
3435
        if (null === $linebreak) {
3436
            $linebreak = static::$LE;
3437
        }
3438
3439
        $mb_length = mb_strlen($str, $this->CharSet);
3440
        //Each line must have length <= 75, including $start and $end
3441
        $length = 75 - strlen($start) - strlen($end);
3442
        //Average multi-byte ratio
3443
        $ratio = $mb_length / strlen($str);
3444
        //Base64 has a 4:3 ratio
3445
        $avgLength = floor($length * $ratio * .75);
3446
3447
        $offset = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $offset is dead and can be removed.
Loading history...
3448
        for ($i = 0; $i < $mb_length; $i += $offset) {
3449
            $lookBack = 0;
3450
            do {
3451
                $offset = $avgLength - $lookBack;
3452
                $chunk = mb_substr($str, $i, $offset, $this->CharSet);
0 ignored issues
show
Bug introduced by
$offset of type double is incompatible with the type integer|null expected by parameter $length of mb_substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3452
                $chunk = mb_substr($str, $i, /** @scrutinizer ignore-type */ $offset, $this->CharSet);
Loading history...
3453
                $chunk = base64_encode($chunk);
3454
                ++$lookBack;
3455
            } while (strlen($chunk) > $length);
3456
            $encoded .= $chunk . $linebreak;
3457
        }
3458
3459
        //Chomp the last linefeed
3460
        return substr($encoded, 0, -strlen($linebreak));
3461
    }
3462
3463
    /**
3464
     * Encode a string in quoted-printable format.
3465
     * According to RFC2045 section 6.7.
3466
     *
3467
     * @param string $string The text to encode
3468
     *
3469
     * @return string
3470
     */
3471
    public function encodeQP($string)
3472
    {
3473
        return static::normalizeBreaks(quoted_printable_encode($string));
3474
    }
3475
3476
    /**
3477
     * Encode a string using Q encoding.
3478
     *
3479
     * @see http://tools.ietf.org/html/rfc2047#section-4.2
3480
     *
3481
     * @param string $str      the text to encode
3482
     * @param string $position Where the text is going to be used, see the RFC for what that means
3483
     *
3484
     * @return string
3485
     */
3486
    public function encodeQ($str, $position = 'text')
3487
    {
3488
        //There should not be any EOL in the string
3489
        $pattern = '';
3490
        $encoded = str_replace(["\r", "\n"], '', $str);
3491
        switch (strtolower($position)) {
3492
            case 'phrase':
3493
                //RFC 2047 section 5.3
3494
                $pattern = '^A-Za-z0-9!*+\/ -';
3495
                break;
3496
            /*
3497
             * RFC 2047 section 5.2.
3498
             * Build $pattern without including delimiters and []
3499
             */
3500
            /* @noinspection PhpMissingBreakStatementInspection */
3501
            case 'comment':
3502
                $pattern = '\(\)"';
3503
            /* Intentional fall through */
3504
            case 'text':
3505
            default:
3506
                //RFC 2047 section 5.1
3507
                //Replace every high ascii, control, =, ? and _ characters
3508
                $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
3509
                break;
3510
        }
3511
        $matches = [];
3512
        if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
3513
            //If the string contains an '=', make sure it's the first thing we replace
3514
            //so as to avoid double-encoding
3515
            $eqkey = array_search('=', $matches[0], true);
3516
            if (false !== $eqkey) {
3517
                unset($matches[0][$eqkey]);
3518
                array_unshift($matches[0], '=');
3519
            }
3520
            foreach (array_unique($matches[0]) as $char) {
3521
                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
3522
            }
3523
        }
3524
        //Replace spaces with _ (more readable than =20)
3525
        //RFC 2047 section 4.2(2)
3526
        return str_replace(' ', '_', $encoded);
3527
    }
3528
3529
    /**
3530
     * Add a string or binary attachment (non-filesystem).
3531
     * This method can be used to attach ascii or binary data,
3532
     * such as a BLOB record from a database.
3533
     *
3534
     * @param string $string      String attachment data
3535
     * @param string $filename    Name of the attachment
3536
     * @param string $encoding    File encoding (see $Encoding)
3537
     * @param string $type        File extension (MIME) type
3538
     * @param string $disposition Disposition to use
3539
     *
3540
     * @throws Exception
3541
     *
3542
     * @return bool True on successfully adding an attachment
3543
     */
3544
    public function addStringAttachment(
3545
        $string,
3546
        $filename,
3547
        $encoding = self::ENCODING_BASE64,
3548
        $type = '',
3549
        $disposition = 'attachment'
3550
    ) {
3551
        try {
3552
            //If a MIME type is not specified, try to work it out from the file name
3553
            if ('' === $type) {
3554
                $type = static::filenameToType($filename);
3555
            }
3556
3557
            if (!$this->validateEncoding($encoding)) {
3558
                throw new Exception($this->lang('encoding') . $encoding);
3559
            }
3560
3561
            //Append to $attachment array
3562
            $this->attachment[] = [
3563
                0 => $string,
3564
                1 => $filename,
3565
                2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
3566
                3 => $encoding,
3567
                4 => $type,
3568
                5 => true, //isStringAttachment
3569
                6 => $disposition,
3570
                7 => 0,
3571
            ];
3572
        } catch (Exception $exc) {
3573
            $this->setError($exc->getMessage());
3574
            $this->edebug($exc->getMessage());
3575
            if ($this->exceptions) {
3576
                throw $exc;
3577
            }
3578
3579
            return false;
3580
        }
3581
3582
        return true;
3583
    }
3584
3585
    /**
3586
     * Add an embedded (inline) attachment from a file.
3587
     * This can include images, sounds, and just about any other document type.
3588
     * These differ from 'regular' attachments in that they are intended to be
3589
     * displayed inline with the message, not just attached for download.
3590
     * This is used in HTML messages that embed the images
3591
     * the HTML refers to using the $cid value.
3592
     * Never use a user-supplied path to a file!
3593
     *
3594
     * @param string $path        Path to the attachment
3595
     * @param string $cid         Content ID of the attachment; Use this to reference
3596
     *                            the content when using an embedded image in HTML
3597
     * @param string $name        Overrides the attachment name
3598
     * @param string $encoding    File encoding (see $Encoding)
3599
     * @param string $type        File MIME type
3600
     * @param string $disposition Disposition to use
3601
     *
3602
     * @throws Exception
3603
     *
3604
     * @return bool True on successfully adding an attachment
3605
     */
3606
    public function addEmbeddedImage(
3607
        $path,
3608
        $cid,
3609
        $name = '',
3610
        $encoding = self::ENCODING_BASE64,
3611
        $type = '',
3612
        $disposition = 'inline'
3613
    ) {
3614
        try {
3615
            if (!static::fileIsAccessible($path)) {
3616
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
3617
            }
3618
3619
            //If a MIME type is not specified, try to work it out from the file name
3620
            if ('' === $type) {
3621
                $type = static::filenameToType($path);
3622
            }
3623
3624
            if (!$this->validateEncoding($encoding)) {
3625
                throw new Exception($this->lang('encoding') . $encoding);
3626
            }
3627
3628
            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
3629
            if ('' === $name) {
3630
                $name = $filename;
3631
            }
3632
3633
            //Append to $attachment array
3634
            $this->attachment[] = [
3635
                0 => $path,
3636
                1 => $filename,
3637
                2 => $name,
3638
                3 => $encoding,
3639
                4 => $type,
3640
                5 => false, //isStringAttachment
3641
                6 => $disposition,
3642
                7 => $cid,
3643
            ];
3644
        } catch (Exception $exc) {
3645
            $this->setError($exc->getMessage());
3646
            $this->edebug($exc->getMessage());
3647
            if ($this->exceptions) {
3648
                throw $exc;
3649
            }
3650
3651
            return false;
3652
        }
3653
3654
        return true;
3655
    }
3656
3657
    /**
3658
     * Add an embedded stringified attachment.
3659
     * This can include images, sounds, and just about any other document type.
3660
     * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.
3661
     *
3662
     * @param string $string      The attachment binary data
3663
     * @param string $cid         Content ID of the attachment; Use this to reference
3664
     *                            the content when using an embedded image in HTML
3665
     * @param string $name        A filename for the attachment. If this contains an extension,
3666
     *                            PHPMailer will attempt to set a MIME type for the attachment.
3667
     *                            For example 'file.jpg' would get an 'image/jpeg' MIME type.
3668
     * @param string $encoding    File encoding (see $Encoding), defaults to 'base64'
3669
     * @param string $type        MIME type - will be used in preference to any automatically derived type
3670
     * @param string $disposition Disposition to use
3671
     *
3672
     * @throws Exception
3673
     *
3674
     * @return bool True on successfully adding an attachment
3675
     */
3676
    public function addStringEmbeddedImage(
3677
        $string,
3678
        $cid,
3679
        $name = '',
3680
        $encoding = self::ENCODING_BASE64,
3681
        $type = '',
3682
        $disposition = 'inline'
3683
    ) {
3684
        try {
3685
            //If a MIME type is not specified, try to work it out from the name
3686
            if ('' === $type && !empty($name)) {
3687
                $type = static::filenameToType($name);
3688
            }
3689
3690
            if (!$this->validateEncoding($encoding)) {
3691
                throw new Exception($this->lang('encoding') . $encoding);
3692
            }
3693
3694
            //Append to $attachment array
3695
            $this->attachment[] = [
3696
                0 => $string,
3697
                1 => $name,
3698
                2 => $name,
3699
                3 => $encoding,
3700
                4 => $type,
3701
                5 => true, //isStringAttachment
3702
                6 => $disposition,
3703
                7 => $cid,
3704
            ];
3705
        } catch (Exception $exc) {
3706
            $this->setError($exc->getMessage());
3707
            $this->edebug($exc->getMessage());
3708
            if ($this->exceptions) {
3709
                throw $exc;
3710
            }
3711
3712
            return false;
3713
        }
3714
3715
        return true;
3716
    }
3717
3718
    /**
3719
     * Validate encodings.
3720
     *
3721
     * @param string $encoding
3722
     *
3723
     * @return bool
3724
     */
3725
    protected function validateEncoding($encoding)
3726
    {
3727
        return in_array(
3728
            $encoding,
3729
            [
3730
                self::ENCODING_7BIT,
3731
                self::ENCODING_QUOTED_PRINTABLE,
3732
                self::ENCODING_BASE64,
3733
                self::ENCODING_8BIT,
3734
                self::ENCODING_BINARY,
3735
            ],
3736
            true
3737
        );
3738
    }
3739
3740
    /**
3741
     * Check if an embedded attachment is present with this cid.
3742
     *
3743
     * @param string $cid
3744
     *
3745
     * @return bool
3746
     */
3747
    protected function cidExists($cid)
3748
    {
3749
        foreach ($this->attachment as $attachment) {
3750
            if ('inline' === $attachment[6] && $cid === $attachment[7]) {
3751
                return true;
3752
            }
3753
        }
3754
3755
        return false;
3756
    }
3757
3758
    /**
3759
     * Check if an inline attachment is present.
3760
     *
3761
     * @return bool
3762
     */
3763
    public function inlineImageExists()
3764
    {
3765
        foreach ($this->attachment as $attachment) {
3766
            if ('inline' === $attachment[6]) {
3767
                return true;
3768
            }
3769
        }
3770
3771
        return false;
3772
    }
3773
3774
    /**
3775
     * Check if an attachment (non-inline) is present.
3776
     *
3777
     * @return bool
3778
     */
3779
    public function attachmentExists()
3780
    {
3781
        foreach ($this->attachment as $attachment) {
3782
            if ('attachment' === $attachment[6]) {
3783
                return true;
3784
            }
3785
        }
3786
3787
        return false;
3788
    }
3789
3790
    /**
3791
     * Check if this message has an alternative body set.
3792
     *
3793
     * @return bool
3794
     */
3795
    public function alternativeExists()
3796
    {
3797
        return !empty($this->AltBody);
3798
    }
3799
3800
    /**
3801
     * Clear queued addresses of given kind.
3802
     *
3803
     * @param string $kind 'to', 'cc', or 'bcc'
3804
     */
3805
    public function clearQueuedAddresses($kind)
3806
    {
3807
        $this->RecipientsQueue = array_filter(
3808
            $this->RecipientsQueue,
3809
            static function ($params) use ($kind) {
3810
                return $params[0] !== $kind;
3811
            }
3812
        );
3813
    }
3814
3815
    /**
3816
     * Clear all To recipients.
3817
     */
3818
    public function clearAddresses()
3819
    {
3820
        foreach ($this->to as $to) {
3821
            unset($this->all_recipients[strtolower($to[0])]);
3822
        }
3823
        $this->to = [];
3824
        $this->clearQueuedAddresses('to');
3825
    }
3826
3827
    /**
3828
     * Clear all CC recipients.
3829
     */
3830
    public function clearCCs()
3831
    {
3832
        foreach ($this->cc as $cc) {
3833
            unset($this->all_recipients[strtolower($cc[0])]);
3834
        }
3835
        $this->cc = [];
3836
        $this->clearQueuedAddresses('cc');
3837
    }
3838
3839
    /**
3840
     * Clear all BCC recipients.
3841
     */
3842
    public function clearBCCs()
3843
    {
3844
        foreach ($this->bcc as $bcc) {
3845
            unset($this->all_recipients[strtolower($bcc[0])]);
3846
        }
3847
        $this->bcc = [];
3848
        $this->clearQueuedAddresses('bcc');
3849
    }
3850
3851
    /**
3852
     * Clear all ReplyTo recipients.
3853
     */
3854
    public function clearReplyTos()
3855
    {
3856
        $this->ReplyTo = [];
3857
        $this->ReplyToQueue = [];
3858
    }
3859
3860
    /**
3861
     * Clear all recipient types.
3862
     */
3863
    public function clearAllRecipients()
3864
    {
3865
        $this->to = [];
3866
        $this->cc = [];
3867
        $this->bcc = [];
3868
        $this->all_recipients = [];
3869
        $this->RecipientsQueue = [];
3870
    }
3871
3872
    /**
3873
     * Clear all filesystem, string, and binary attachments.
3874
     */
3875
    public function clearAttachments()
3876
    {
3877
        $this->attachment = [];
3878
    }
3879
3880
    /**
3881
     * Clear all custom headers.
3882
     */
3883
    public function clearCustomHeaders()
3884
    {
3885
        $this->CustomHeader = [];
3886
    }
3887
3888
    /**
3889
     * Add an error message to the error container.
3890
     *
3891
     * @param string $msg
3892
     */
3893
    protected function setError($msg)
3894
    {
3895
        ++$this->error_count;
3896
        if ('smtp' === $this->Mailer && null !== $this->smtp) {
3897
            $lasterror = $this->smtp->getError();
3898
            if (!empty($lasterror['error'])) {
3899
                $msg .= $this->lang('smtp_error') . $lasterror['error'];
3900
                if (!empty($lasterror['detail'])) {
3901
                    $msg .= ' Detail: ' . $lasterror['detail'];
3902
                }
3903
                if (!empty($lasterror['smtp_code'])) {
3904
                    $msg .= ' SMTP code: ' . $lasterror['smtp_code'];
3905
                }
3906
                if (!empty($lasterror['smtp_code_ex'])) {
3907
                    $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex'];
3908
                }
3909
            }
3910
        }
3911
        $this->ErrorInfo = $msg;
3912
    }
3913
3914
    /**
3915
     * Return an RFC 822 formatted date.
3916
     *
3917
     * @return string
3918
     */
3919
    public static function rfcDate()
3920
    {
3921
        //Set the time zone to whatever the default is to avoid 500 errors
3922
        //Will default to UTC if it's not set properly in php.ini
3923
        date_default_timezone_set(@date_default_timezone_get());
3924
3925
        return date('D, j M Y H:i:s O');
3926
    }
3927
3928
    /**
3929
     * Get the server hostname.
3930
     * Returns 'localhost.localdomain' if unknown.
3931
     *
3932
     * @return string
3933
     */
3934
    protected function serverHostname()
3935
    {
3936
        $result = '';
3937
        if (!empty($this->Hostname)) {
3938
            $result = $this->Hostname;
3939
        } elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) {
3940
            $result = $_SERVER['SERVER_NAME'];
3941
        } elseif (function_exists('gethostname') && gethostname() !== false) {
3942
            $result = gethostname();
3943
        } elseif (php_uname('n') !== false) {
3944
            $result = php_uname('n');
3945
        }
3946
        if (!static::isValidHost($result)) {
3947
            return 'localhost.localdomain';
3948
        }
3949
3950
        return $result;
3951
    }
3952
3953
    /**
3954
     * Validate whether a string contains a valid value to use as a hostname or IP address.
3955
     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
3956
     *
3957
     * @param string $host The host name or IP address to check
3958
     *
3959
     * @return bool
3960
     */
3961
    public static function isValidHost($host)
3962
    {
3963
        //Simple syntax limits
3964
        if (
3965
            empty($host)
3966
            || !is_string($host)
3967
            || strlen($host) > 256
3968
            || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+])$/', $host)
3969
        ) {
3970
            return false;
3971
        }
3972
        //Looks like a bracketed IPv6 address
3973
        if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') {
3974
            return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
3975
        }
3976
        //If removing all the dots results in a numeric string, it must be an IPv4 address.
3977
        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
3978
        if (is_numeric(str_replace('.', '', $host))) {
3979
            //Is it a valid IPv4 address?
3980
            return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
3981
        }
3982
        if (filter_var('http://' . $host, FILTER_VALIDATE_URL) !== false) {
3983
            //Is it a syntactically valid hostname?
3984
            return true;
3985
        }
3986
3987
        return false;
3988
    }
3989
3990
    /**
3991
     * Get an error message in the current language.
3992
     *
3993
     * @param string $key
3994
     *
3995
     * @return string
3996
     */
3997
    protected function lang($key)
3998
    {
3999
        if (count($this->language) < 1) {
4000
            $this->setLanguage(); //Set the default language
4001
        }
4002
4003
        if (array_key_exists($key, $this->language)) {
4004
            if ('smtp_connect_failed' === $key) {
4005
                //Include a link to troubleshooting docs on SMTP connection failure.
4006
                //This is by far the biggest cause of support questions
4007
                //but it's usually not PHPMailer's fault.
4008
                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
4009
            }
4010
4011
            return $this->language[$key];
4012
        }
4013
4014
        //Return the key as a fallback
4015
        return $key;
4016
    }
4017
4018
    /**
4019
     * Check if an error occurred.
4020
     *
4021
     * @return bool True if an error did occur
4022
     */
4023
    public function isError()
4024
    {
4025
        return $this->error_count > 0;
4026
    }
4027
4028
    /**
4029
     * Add a custom header.
4030
     * $name value can be overloaded to contain
4031
     * both header name and value (name:value).
4032
     *
4033
     * @param string      $name  Custom header name
4034
     * @param string|null $value Header value
4035
     *
4036
     * @throws Exception
4037
     */
4038
    public function addCustomHeader($name, $value = null)
4039
    {
4040
        if (null === $value && strpos($name, ':') !== false) {
4041
            //Value passed in as name:value
4042
            list($name, $value) = explode(':', $name, 2);
4043
        }
4044
        $name = trim($name);
4045
        $value = trim($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type null; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4045
        $value = trim(/** @scrutinizer ignore-type */ $value);
Loading history...
4046
        //Ensure name is not empty, and that neither name nor value contain line breaks
4047
        if (empty($name) || strpbrk($name . $value, "\r\n") !== false) {
4048
            if ($this->exceptions) {
4049
                throw new Exception('Invalid header name or value');
4050
            }
4051
4052
            return false;
4053
        }
4054
        $this->CustomHeader[] = [$name, $value];
4055
4056
        return true;
4057
    }
4058
4059
    /**
4060
     * Returns all custom headers.
4061
     *
4062
     * @return array
4063
     */
4064
    public function getCustomHeaders()
4065
    {
4066
        return $this->CustomHeader;
4067
    }
4068
4069
    /**
4070
     * Create a message body from an HTML string.
4071
     * Automatically inlines images and creates a plain-text version by converting the HTML,
4072
     * overwriting any existing values in Body and AltBody.
4073
     * Do not source $message content from user input!
4074
     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
4075
     * will look for an image file in $basedir/images/a.png and convert it to inline.
4076
     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
4077
     * Converts data-uri images into embedded attachments.
4078
     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
4079
     *
4080
     * @param string        $message  HTML message string
4081
     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
4082
     * @param bool|callable $advanced Whether to use the internal HTML to text converter
4083
     *                                or your own custom converter
4084
     * @return string The transformed message body
4085
     *
4086
     * @throws Exception
4087
     *
4088
     * @see PHPMailer::html2text()
4089
     */
4090
    public function msgHTML($message, $basedir = '', $advanced = false)
4091
    {
4092
        preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
4093
        if (array_key_exists(2, $images)) {
4094
            if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
4095
                //Ensure $basedir has a trailing /
4096
                $basedir .= '/';
4097
            }
4098
            foreach ($images[2] as $imgindex => $url) {
4099
                //Convert data URIs into embedded images
4100
                //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
4101
                $match = [];
4102
                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
4103
                    if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
4104
                        $data = base64_decode($match[3]);
4105
                    } elseif ('' === $match[2]) {
4106
                        $data = rawurldecode($match[3]);
4107
                    } else {
4108
                        //Not recognised so leave it alone
4109
                        continue;
4110
                    }
4111
                    //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
4112
                    //will only be embedded once, even if it used a different encoding
4113
                    $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; //RFC2392 S 2
4114
4115
                    if (!$this->cidExists($cid)) {
4116
                        $this->addStringEmbeddedImage(
4117
                            $data,
4118
                            $cid,
4119
                            'embed' . $imgindex,
4120
                            static::ENCODING_BASE64,
4121
                            $match[1]
4122
                        );
4123
                    }
4124
                    $message = str_replace(
4125
                        $images[0][$imgindex],
4126
                        $images[1][$imgindex] . '="cid:' . $cid . '"',
4127
                        $message
4128
                    );
4129
                    continue;
4130
                }
4131
                if (
4132
                    //Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
4133
                    !empty($basedir)
4134
                    //Ignore URLs containing parent dir traversal (..)
4135
                    && (strpos($url, '..') === false)
4136
                    //Do not change urls that are already inline images
4137
                    && 0 !== strpos($url, 'cid:')
4138
                    //Do not change absolute URLs, including anonymous protocol
4139
                    && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
4140
                ) {
4141
                    $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
4142
                    $directory = dirname($url);
4143
                    if ('.' === $directory) {
4144
                        $directory = '';
4145
                    }
4146
                    //RFC2392 S 2
4147
                    $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
4148
                    if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
4149
                        $basedir .= '/';
4150
                    }
4151
                    if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
4152
                        $directory .= '/';
4153
                    }
4154
                    if (
4155
                        $this->addEmbeddedImage(
4156
                            $basedir . $directory . $filename,
0 ignored issues
show
Bug introduced by
Are you sure $filename of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4156
                            $basedir . $directory . /** @scrutinizer ignore-type */ $filename,
Loading history...
4157
                            $cid,
4158
                            $filename,
0 ignored issues
show
Bug introduced by
It seems like $filename can also be of type array; however, parameter $name of PHPMailer\PHPMailer\PHPMailer::addEmbeddedImage() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4158
                            /** @scrutinizer ignore-type */ $filename,
Loading history...
4159
                            static::ENCODING_BASE64,
4160
                            static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
0 ignored issues
show
Bug introduced by
It seems like $filename can also be of type array; however, parameter $path of PHPMailer\PHPMailer\PHPMailer::mb_pathinfo() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4160
                            static::_mime_types((string) static::mb_pathinfo(/** @scrutinizer ignore-type */ $filename, PATHINFO_EXTENSION))
Loading history...
4161
                        )
4162
                    ) {
4163
                        $message = preg_replace(
4164
                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
4165
                            $images[1][$imgindex] . '="cid:' . $cid . '"',
4166
                            $message
4167
                        );
4168
                    }
4169
                }
4170
            }
4171
        }
4172
        $this->isHTML();
4173
        //Convert all message body line breaks to LE, makes quoted-printable encoding work much better
4174
        $this->Body = static::normalizeBreaks($message);
4175
        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
4176
        if (!$this->alternativeExists()) {
4177
            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
4178
                . static::$LE;
4179
        }
4180
4181
        return $this->Body;
4182
    }
4183
4184
    /**
4185
     * Convert an HTML string into plain text.
4186
     * This is used by msgHTML().
4187
     * Note - older versions of this function used a bundled advanced converter
4188
     * which was removed for license reasons in #232.
4189
     * Example usage:
4190
     *
4191
     * ```php
4192
     * //Use default conversion
4193
     * $plain = $mail->html2text($html);
4194
     * //Use your own custom converter
4195
     * $plain = $mail->html2text($html, function($html) {
4196
     *     $converter = new MyHtml2text($html);
4197
     *     return $converter->get_text();
4198
     * });
4199
     * ```
4200
     *
4201
     * @param string        $html     The HTML text to convert
4202
     * @param bool|callable $advanced Any boolean value to use the internal converter,
4203
     *                                or provide your own callable for custom conversion
4204
     *
4205
     * @return string
4206
     */
4207
    public function html2text($html, $advanced = false)
4208
    {
4209
        if (is_callable($advanced)) {
4210
            return call_user_func($advanced, $html);
0 ignored issues
show
Bug introduced by
It seems like $advanced can also be of type boolean; however, parameter $callback of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4210
            return call_user_func(/** @scrutinizer ignore-type */ $advanced, $html);
Loading history...
4211
        }
4212
4213
        return html_entity_decode(
4214
            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
4215
            ENT_QUOTES,
4216
            $this->CharSet
4217
        );
4218
    }
4219
4220
    /**
4221
     * Get the MIME type for a file extension.
4222
     *
4223
     * @param string $ext File extension
4224
     *
4225
     * @return string MIME type of file
4226
     */
4227
    public static function _mime_types($ext = '')
4228
    {
4229
        $mimes = [
4230
            'xl' => 'application/excel',
4231
            'js' => 'application/javascript',
4232
            'hqx' => 'application/mac-binhex40',
4233
            'cpt' => 'application/mac-compactpro',
4234
            'bin' => 'application/macbinary',
4235
            'doc' => 'application/msword',
4236
            'word' => 'application/msword',
4237
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
4238
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
4239
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
4240
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
4241
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
4242
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
4243
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
4244
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
4245
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
4246
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
4247
            'class' => 'application/octet-stream',
4248
            'dll' => 'application/octet-stream',
4249
            'dms' => 'application/octet-stream',
4250
            'exe' => 'application/octet-stream',
4251
            'lha' => 'application/octet-stream',
4252
            'lzh' => 'application/octet-stream',
4253
            'psd' => 'application/octet-stream',
4254
            'sea' => 'application/octet-stream',
4255
            'so' => 'application/octet-stream',
4256
            'oda' => 'application/oda',
4257
            'pdf' => 'application/pdf',
4258
            'ai' => 'application/postscript',
4259
            'eps' => 'application/postscript',
4260
            'ps' => 'application/postscript',
4261
            'smi' => 'application/smil',
4262
            'smil' => 'application/smil',
4263
            'mif' => 'application/vnd.mif',
4264
            'xls' => 'application/vnd.ms-excel',
4265
            'ppt' => 'application/vnd.ms-powerpoint',
4266
            'wbxml' => 'application/vnd.wap.wbxml',
4267
            'wmlc' => 'application/vnd.wap.wmlc',
4268
            'dcr' => 'application/x-director',
4269
            'dir' => 'application/x-director',
4270
            'dxr' => 'application/x-director',
4271
            'dvi' => 'application/x-dvi',
4272
            'gtar' => 'application/x-gtar',
4273
            'php3' => 'application/x-httpd-php',
4274
            'php4' => 'application/x-httpd-php',
4275
            'php' => 'application/x-httpd-php',
4276
            'phtml' => 'application/x-httpd-php',
4277
            'phps' => 'application/x-httpd-php-source',
4278
            'swf' => 'application/x-shockwave-flash',
4279
            'sit' => 'application/x-stuffit',
4280
            'tar' => 'application/x-tar',
4281
            'tgz' => 'application/x-tar',
4282
            'xht' => 'application/xhtml+xml',
4283
            'xhtml' => 'application/xhtml+xml',
4284
            'zip' => 'application/zip',
4285
            'mid' => 'audio/midi',
4286
            'midi' => 'audio/midi',
4287
            'mp2' => 'audio/mpeg',
4288
            'mp3' => 'audio/mpeg',
4289
            'm4a' => 'audio/mp4',
4290
            'mpga' => 'audio/mpeg',
4291
            'aif' => 'audio/x-aiff',
4292
            'aifc' => 'audio/x-aiff',
4293
            'aiff' => 'audio/x-aiff',
4294
            'ram' => 'audio/x-pn-realaudio',
4295
            'rm' => 'audio/x-pn-realaudio',
4296
            'rpm' => 'audio/x-pn-realaudio-plugin',
4297
            'ra' => 'audio/x-realaudio',
4298
            'wav' => 'audio/x-wav',
4299
            'mka' => 'audio/x-matroska',
4300
            'bmp' => 'image/bmp',
4301
            'gif' => 'image/gif',
4302
            'jpeg' => 'image/jpeg',
4303
            'jpe' => 'image/jpeg',
4304
            'jpg' => 'image/jpeg',
4305
            'png' => 'image/png',
4306
            'tiff' => 'image/tiff',
4307
            'tif' => 'image/tiff',
4308
            'webp' => 'image/webp',
4309
            'avif' => 'image/avif',
4310
            'heif' => 'image/heif',
4311
            'heifs' => 'image/heif-sequence',
4312
            'heic' => 'image/heic',
4313
            'heics' => 'image/heic-sequence',
4314
            'eml' => 'message/rfc822',
4315
            'css' => 'text/css',
4316
            'html' => 'text/html',
4317
            'htm' => 'text/html',
4318
            'shtml' => 'text/html',
4319
            'log' => 'text/plain',
4320
            'text' => 'text/plain',
4321
            'txt' => 'text/plain',
4322
            'rtx' => 'text/richtext',
4323
            'rtf' => 'text/rtf',
4324
            'vcf' => 'text/vcard',
4325
            'vcard' => 'text/vcard',
4326
            'ics' => 'text/calendar',
4327
            'xml' => 'text/xml',
4328
            'xsl' => 'text/xml',
4329
            'wmv' => 'video/x-ms-wmv',
4330
            'mpeg' => 'video/mpeg',
4331
            'mpe' => 'video/mpeg',
4332
            'mpg' => 'video/mpeg',
4333
            'mp4' => 'video/mp4',
4334
            'm4v' => 'video/mp4',
4335
            'mov' => 'video/quicktime',
4336
            'qt' => 'video/quicktime',
4337
            'rv' => 'video/vnd.rn-realvideo',
4338
            'avi' => 'video/x-msvideo',
4339
            'movie' => 'video/x-sgi-movie',
4340
            'webm' => 'video/webm',
4341
            'mkv' => 'video/x-matroska',
4342
        ];
4343
        $ext = strtolower($ext);
4344
        if (array_key_exists($ext, $mimes)) {
4345
            return $mimes[$ext];
4346
        }
4347
4348
        return 'application/octet-stream';
4349
    }
4350
4351
    /**
4352
     * Map a file name to a MIME type.
4353
     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
4354
     *
4355
     * @param string $filename A file name or full path, does not need to exist as a file
4356
     *
4357
     * @return string
4358
     */
4359
    public static function filenameToType($filename)
4360
    {
4361
        //In case the path is a URL, strip any query string before getting extension
4362
        $qpos = strpos($filename, '?');
4363
        if (false !== $qpos) {
4364
            $filename = substr($filename, 0, $qpos);
4365
        }
4366
        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
4367
4368
        return static::_mime_types($ext);
0 ignored issues
show
Bug introduced by
It seems like $ext can also be of type array; however, parameter $ext of PHPMailer\PHPMailer\PHPMailer::_mime_types() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4368
        return static::_mime_types(/** @scrutinizer ignore-type */ $ext);
Loading history...
4369
    }
4370
4371
    /**
4372
     * Multi-byte-safe pathinfo replacement.
4373
     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
4374
     *
4375
     * @see http://www.php.net/manual/en/function.pathinfo.php#107461
4376
     *
4377
     * @param string     $path    A filename or path, does not need to exist as a file
4378
     * @param int|string $options Either a PATHINFO_* constant,
4379
     *                            or a string name to return only the specified piece
4380
     *
4381
     * @return string|array
4382
     */
4383
    public static function mb_pathinfo($path, $options = null)
4384
    {
4385
        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
4386
        $pathinfo = [];
4387
        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
4388
            if (array_key_exists(1, $pathinfo)) {
4389
                $ret['dirname'] = $pathinfo[1];
4390
            }
4391
            if (array_key_exists(2, $pathinfo)) {
4392
                $ret['basename'] = $pathinfo[2];
4393
            }
4394
            if (array_key_exists(5, $pathinfo)) {
4395
                $ret['extension'] = $pathinfo[5];
4396
            }
4397
            if (array_key_exists(3, $pathinfo)) {
4398
                $ret['filename'] = $pathinfo[3];
4399
            }
4400
        }
4401
        switch ($options) {
4402
            case PATHINFO_DIRNAME:
4403
            case 'dirname':
4404
                return $ret['dirname'];
4405
            case PATHINFO_BASENAME:
4406
            case 'basename':
4407
                return $ret['basename'];
4408
            case PATHINFO_EXTENSION:
4409
            case 'extension':
4410
                return $ret['extension'];
4411
            case PATHINFO_FILENAME:
4412
            case 'filename':
4413
                return $ret['filename'];
4414
            default:
4415
                return $ret;
4416
        }
4417
    }
4418
4419
    /**
4420
     * Set or reset instance properties.
4421
     * You should avoid this function - it's more verbose, less efficient, more error-prone and
4422
     * harder to debug than setting properties directly.
4423
     * Usage Example:
4424
     * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`
4425
     *   is the same as:
4426
     * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.
4427
     *
4428
     * @param string $name  The property name to set
4429
     * @param mixed  $value The value to set the property to
4430
     *
4431
     * @return bool
4432
     */
4433
    public function set($name, $value = '')
4434
    {
4435
        if (property_exists($this, $name)) {
4436
            $this->$name = $value;
4437
4438
            return true;
4439
        }
4440
        $this->setError($this->lang('variable_set') . $name);
4441
4442
        return false;
4443
    }
4444
4445
    /**
4446
     * Strip newlines to prevent header injection.
4447
     *
4448
     * @param string $str
4449
     *
4450
     * @return string
4451
     */
4452
    public function secureHeader($str)
4453
    {
4454
        return trim(str_replace(["\r", "\n"], '', $str));
4455
    }
4456
4457
    /**
4458
     * Normalize line breaks in a string.
4459
     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
4460
     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
4461
     *
4462
     * @param string $text
4463
     * @param string $breaktype What kind of line break to use; defaults to static::$LE
4464
     *
4465
     * @return string
4466
     */
4467
    public static function normalizeBreaks($text, $breaktype = null)
4468
    {
4469
        if (null === $breaktype) {
4470
            $breaktype = static::$LE;
4471
        }
4472
        //Normalise to \n
4473
        $text = str_replace([self::CRLF, "\r"], "\n", $text);
4474
        //Now convert LE as needed
4475
        if ("\n" !== $breaktype) {
4476
            $text = str_replace("\n", $breaktype, $text);
4477
        }
4478
4479
        return $text;
4480
    }
4481
4482
    /**
4483
     * Remove trailing breaks from a string.
4484
     *
4485
     * @param string $text
4486
     *
4487
     * @return string The text to remove breaks from
4488
     */
4489
    public static function stripTrailingWSP($text)
4490
    {
4491
        return rtrim($text, " \r\n\t");
4492
    }
4493
4494
    /**
4495
     * Return the current line break format string.
4496
     *
4497
     * @return string
4498
     */
4499
    public static function getLE()
4500
    {
4501
        return static::$LE;
4502
    }
4503
4504
    /**
4505
     * Set the line break format string, e.g. "\r\n".
4506
     *
4507
     * @param string $le
4508
     */
4509
    protected static function setLE($le)
4510
    {
4511
        static::$LE = $le;
4512
    }
4513
4514
    /**
4515
     * Set the public and private key files and password for S/MIME signing.
4516
     *
4517
     * @param string $cert_filename
4518
     * @param string $key_filename
4519
     * @param string $key_pass            Password for private key
4520
     * @param string $extracerts_filename Optional path to chain certificate
4521
     */
4522
    public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
4523
    {
4524
        $this->sign_cert_file = $cert_filename;
4525
        $this->sign_key_file = $key_filename;
4526
        $this->sign_key_pass = $key_pass;
4527
        $this->sign_extracerts_file = $extracerts_filename;
4528
    }
4529
4530
    /**
4531
     * Quoted-Printable-encode a DKIM header.
4532
     *
4533
     * @param string $txt
4534
     *
4535
     * @return string
4536
     */
4537
    public function DKIM_QP($txt)
4538
    {
4539
        $line = '';
4540
        $len = strlen($txt);
4541
        for ($i = 0; $i < $len; ++$i) {
4542
            $ord = ord($txt[$i]);
4543
            if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
4544
                $line .= $txt[$i];
4545
            } else {
4546
                $line .= '=' . sprintf('%02X', $ord);
4547
            }
4548
        }
4549
4550
        return $line;
4551
    }
4552
4553
    /**
4554
     * Generate a DKIM signature.
4555
     *
4556
     * @param string $signHeader
4557
     *
4558
     * @throws Exception
4559
     *
4560
     * @return string The DKIM signature value
4561
     */
4562
    public function DKIM_Sign($signHeader)
4563
    {
4564
        if (!defined('PKCS7_TEXT')) {
4565
            if ($this->exceptions) {
4566
                throw new Exception($this->lang('extension_missing') . 'openssl');
4567
            }
4568
4569
            return '';
4570
        }
4571
        $privKeyStr = !empty($this->DKIM_private_string) ?
4572
            $this->DKIM_private_string :
4573
            file_get_contents($this->DKIM_private);
4574
        if ('' !== $this->DKIM_passphrase) {
4575
            $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
4576
        } else {
4577
            $privKey = openssl_pkey_get_private($privKeyStr);
4578
        }
4579
        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
4580
            if (\PHP_MAJOR_VERSION < 8) {
4581
                openssl_pkey_free($privKey);
4582
            }
4583
4584
            return base64_encode($signature);
4585
        }
4586
        if (\PHP_MAJOR_VERSION < 8) {
4587
            openssl_pkey_free($privKey);
4588
        }
4589
4590
        return '';
4591
    }
4592
4593
    /**
4594
     * Generate a DKIM canonicalization header.
4595
     * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
4596
     * Canonicalized headers should *always* use CRLF, regardless of mailer setting.
4597
     *
4598
     * @see https://tools.ietf.org/html/rfc6376#section-3.4.2
4599
     *
4600
     * @param string $signHeader Header
4601
     *
4602
     * @return string
4603
     */
4604
    public function DKIM_HeaderC($signHeader)
4605
    {
4606
        //Normalize breaks to CRLF (regardless of the mailer)
4607
        $signHeader = static::normalizeBreaks($signHeader, self::CRLF);
4608
        //Unfold header lines
4609
        //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
4610
        //@see https://tools.ietf.org/html/rfc5322#section-2.2
4611
        //That means this may break if you do something daft like put vertical tabs in your headers.
4612
        $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
4613
        //Break headers out into an array
4614
        $lines = explode(self::CRLF, $signHeader);
4615
        foreach ($lines as $key => $line) {
4616
            //If the header is missing a :, skip it as it's invalid
4617
            //This is likely to happen because the explode() above will also split
4618
            //on the trailing LE, leaving an empty line
4619
            if (strpos($line, ':') === false) {
4620
                continue;
4621
            }
4622
            list($heading, $value) = explode(':', $line, 2);
4623
            //Lower-case header name
4624
            $heading = strtolower($heading);
4625
            //Collapse white space within the value, also convert WSP to space
4626
            $value = preg_replace('/[ \t]+/', ' ', $value);
4627
            //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
4628
            //But then says to delete space before and after the colon.
4629
            //Net result is the same as trimming both ends of the value.
4630
            //By elimination, the same applies to the field name
4631
            $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
4632
        }
4633
4634
        return implode(self::CRLF, $lines);
4635
    }
4636
4637
    /**
4638
     * Generate a DKIM canonicalization body.
4639
     * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
4640
     * Canonicalized bodies should *always* use CRLF, regardless of mailer setting.
4641
     *
4642
     * @see https://tools.ietf.org/html/rfc6376#section-3.4.3
4643
     *
4644
     * @param string $body Message Body
4645
     *
4646
     * @return string
4647
     */
4648
    public function DKIM_BodyC($body)
4649
    {
4650
        if (empty($body)) {
4651
            return self::CRLF;
4652
        }
4653
        //Normalize line endings to CRLF
4654
        $body = static::normalizeBreaks($body, self::CRLF);
4655
4656
        //Reduce multiple trailing line breaks to a single one
4657
        return static::stripTrailingWSP($body) . self::CRLF;
4658
    }
4659
4660
    /**
4661
     * Create the DKIM header and body in a new message header.
4662
     *
4663
     * @param string $headers_line Header lines
4664
     * @param string $subject      Subject
4665
     * @param string $body         Body
4666
     *
4667
     * @throws Exception
4668
     *
4669
     * @return string
4670
     */
4671
    public function DKIM_Add($headers_line, $subject, $body)
4672
    {
4673
        $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms
4674
        $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body
4675
        $DKIMquery = 'dns/txt'; //Query method
4676
        $DKIMtime = time();
4677
        //Always sign these headers without being asked
4678
        //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1
4679
        $autoSignHeaders = [
4680
            'from',
4681
            'to',
4682
            'cc',
4683
            'date',
4684
            'subject',
4685
            'reply-to',
4686
            'message-id',
4687
            'content-type',
4688
            'mime-version',
4689
            'x-mailer',
4690
        ];
4691
        if (stripos($headers_line, 'Subject') === false) {
4692
            $headers_line .= 'Subject: ' . $subject . static::$LE;
4693
        }
4694
        $headerLines = explode(static::$LE, $headers_line);
4695
        $currentHeaderLabel = '';
4696
        $currentHeaderValue = '';
4697
        $parsedHeaders = [];
4698
        $headerLineIndex = 0;
4699
        $headerLineCount = count($headerLines);
4700
        foreach ($headerLines as $headerLine) {
4701
            $matches = [];
4702
            if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
4703
                if ($currentHeaderLabel !== '') {
4704
                    //We were previously in another header; This is the start of a new header, so save the previous one
4705
                    $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
4706
                }
4707
                $currentHeaderLabel = $matches[1];
4708
                $currentHeaderValue = $matches[2];
4709
            } elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
4710
                //This is a folded continuation of the current header, so unfold it
4711
                $currentHeaderValue .= ' ' . $matches[1];
4712
            }
4713
            ++$headerLineIndex;
4714
            if ($headerLineIndex >= $headerLineCount) {
4715
                //This was the last line, so finish off this header
4716
                $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
4717
            }
4718
        }
4719
        $copiedHeaders = [];
4720
        $headersToSignKeys = [];
4721
        $headersToSign = [];
4722
        foreach ($parsedHeaders as $header) {
4723
            //Is this header one that must be included in the DKIM signature?
4724
            if (in_array(strtolower($header['label']), $autoSignHeaders, true)) {
4725
                $headersToSignKeys[] = $header['label'];
4726
                $headersToSign[] = $header['label'] . ': ' . $header['value'];
4727
                if ($this->DKIM_copyHeaderFields) {
4728
                    $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
4729
                        str_replace('|', '=7C', $this->DKIM_QP($header['value']));
4730
                }
4731
                continue;
4732
            }
4733
            //Is this an extra custom header we've been asked to sign?
4734
            if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {
4735
                //Find its value in custom headers
4736
                foreach ($this->CustomHeader as $customHeader) {
4737
                    if ($customHeader[0] === $header['label']) {
4738
                        $headersToSignKeys[] = $header['label'];
4739
                        $headersToSign[] = $header['label'] . ': ' . $header['value'];
4740
                        if ($this->DKIM_copyHeaderFields) {
4741
                            $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
4742
                                str_replace('|', '=7C', $this->DKIM_QP($header['value']));
4743
                        }
4744
                        //Skip straight to the next header
4745
                        continue 2;
4746
                    }
4747
                }
4748
            }
4749
        }
4750
        $copiedHeaderFields = '';
4751
        if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {
4752
            //Assemble a DKIM 'z' tag
4753
            $copiedHeaderFields = ' z=';
4754
            $first = true;
4755
            foreach ($copiedHeaders as $copiedHeader) {
4756
                if (!$first) {
4757
                    $copiedHeaderFields .= static::$LE . ' |';
4758
                }
4759
                //Fold long values
4760
                if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {
4761
                    $copiedHeaderFields .= substr(
4762
                        chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS),
4763
                        0,
4764
                        -strlen(static::$LE . self::FWS)
4765
                    );
4766
                } else {
4767
                    $copiedHeaderFields .= $copiedHeader;
4768
                }
4769
                $first = false;
4770
            }
4771
            $copiedHeaderFields .= ';' . static::$LE;
4772
        }
4773
        $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
4774
        $headerValues = implode(static::$LE, $headersToSign);
4775
        $body = $this->DKIM_BodyC($body);
4776
        //Base64 of packed binary SHA-256 hash of body
4777
        $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body)));
4778
        $ident = '';
4779
        if ('' !== $this->DKIM_identity) {
4780
            $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
4781
        }
4782
        //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
4783
        //which is appended after calculating the signature
4784
        //https://tools.ietf.org/html/rfc6376#section-3.5
4785
        $dkimSignatureHeader = 'DKIM-Signature: v=1;' .
4786
            ' d=' . $this->DKIM_domain . ';' .
4787
            ' s=' . $this->DKIM_selector . ';' . static::$LE .
4788
            ' a=' . $DKIMsignatureType . ';' .
4789
            ' q=' . $DKIMquery . ';' .
4790
            ' t=' . $DKIMtime . ';' .
4791
            ' c=' . $DKIMcanonicalization . ';' . static::$LE .
4792
            $headerKeys .
4793
            $ident .
4794
            $copiedHeaderFields .
4795
            ' bh=' . $DKIMb64 . ';' . static::$LE .
4796
            ' b=';
4797
        //Canonicalize the set of headers
4798
        $canonicalizedHeaders = $this->DKIM_HeaderC(
4799
            $headerValues . static::$LE . $dkimSignatureHeader
4800
        );
4801
        $signature = $this->DKIM_Sign($canonicalizedHeaders);
4802
        $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS));
4803
4804
        return static::normalizeBreaks($dkimSignatureHeader . $signature);
4805
    }
4806
4807
    /**
4808
     * Detect if a string contains a line longer than the maximum line length
4809
     * allowed by RFC 2822 section 2.1.1.
4810
     *
4811
     * @param string $str
4812
     *
4813
     * @return bool
4814
     */
4815
    public static function hasLineLongerThanMax($str)
4816
    {
4817
        return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
4818
    }
4819
4820
    /**
4821
     * If a string contains any "special" characters, double-quote the name,
4822
     * and escape any double quotes with a backslash.
4823
     *
4824
     * @param string $str
4825
     *
4826
     * @return string
4827
     *
4828
     * @see RFC822 3.4.1
4829
     */
4830
    public static function quotedString($str)
4831
    {
4832
        if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) {
4833
            //If the string contains any of these chars, it must be double-quoted
4834
            //and any double quotes must be escaped with a backslash
4835
            return '"' . str_replace('"', '\\"', $str) . '"';
4836
        }
4837
4838
        //Return the string untouched, it doesn't need quoting
4839
        return $str;
4840
    }
4841
4842
    /**
4843
     * Allows for public read access to 'to' property.
4844
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4845
     *
4846
     * @return array
4847
     */
4848
    public function getToAddresses()
4849
    {
4850
        return $this->to;
4851
    }
4852
4853
    /**
4854
     * Allows for public read access to 'cc' property.
4855
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4856
     *
4857
     * @return array
4858
     */
4859
    public function getCcAddresses()
4860
    {
4861
        return $this->cc;
4862
    }
4863
4864
    /**
4865
     * Allows for public read access to 'bcc' property.
4866
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4867
     *
4868
     * @return array
4869
     */
4870
    public function getBccAddresses()
4871
    {
4872
        return $this->bcc;
4873
    }
4874
4875
    /**
4876
     * Allows for public read access to 'ReplyTo' property.
4877
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4878
     *
4879
     * @return array
4880
     */
4881
    public function getReplyToAddresses()
4882
    {
4883
        return $this->ReplyTo;
4884
    }
4885
4886
    /**
4887
     * Allows for public read access to 'all_recipients' property.
4888
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4889
     *
4890
     * @return array
4891
     */
4892
    public function getAllRecipientAddresses()
4893
    {
4894
        return $this->all_recipients;
4895
    }
4896
4897
    /**
4898
     * Perform a callback.
4899
     *
4900
     * @param bool   $isSent
4901
     * @param array  $to
4902
     * @param array  $cc
4903
     * @param array  $bcc
4904
     * @param string $subject
4905
     * @param string $body
4906
     * @param string $from
4907
     * @param array  $extra
4908
     */
4909
    protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
4910
    {
4911
        if (!empty($this->action_function) && is_callable($this->action_function)) {
4912
            call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);
4913
        }
4914
    }
4915
4916
    /**
4917
     * Get the OAuth instance.
4918
     *
4919
     * @return OAuth
4920
     */
4921
    public function getOAuth()
4922
    {
4923
        return $this->oauth;
4924
    }
4925
4926
    /**
4927
     * Set an OAuth instance.
4928
     */
4929
    public function setOAuth(OAuth $oauth)
4930
    {
4931
        $this->oauth = $oauth;
4932
    }
4933
}
4934