PHPMailer::isHTML()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 6
rs 10
c 1
b 0
f 0
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   https://www.gnu.org/licenses/old-licenses/lgpl-2.1.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 = '';
107
108
    /**
109
     * The From name of the message.
110
     *
111
     * @var string
112
     */
113
    public $FromName = '';
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 https://kigkonsult.se/iCalcreator/
156
     *
157
     * @var string
158
     */
159
    public $Ical = '';
160
161
    /**
162
     * Value-array of "method" in Contenttype header "text/calendar"
163
     *
164
     * @var string[]
165
     */
166
    protected static $IcalMethods = [
167
        self::ICAL_METHOD_REQUEST,
168
        self::ICAL_METHOD_PUBLISH,
169
        self::ICAL_METHOD_REPLY,
170
        self::ICAL_METHOD_ADD,
171
        self::ICAL_METHOD_CANCEL,
172
        self::ICAL_METHOD_REFRESH,
173
        self::ICAL_METHOD_COUNTER,
174
        self::ICAL_METHOD_DECLINECOUNTER,
175
    ];
176
177
    /**
178
     * The complete compiled MIME message body.
179
     *
180
     * @var string
181
     */
182
    protected $MIMEBody = '';
183
184
    /**
185
     * The complete compiled MIME message headers.
186
     *
187
     * @var string
188
     */
189
    protected $MIMEHeader = '';
190
191
    /**
192
     * Extra headers that createHeader() doesn't fold in.
193
     *
194
     * @var string
195
     */
196
    protected $mailHeader = '';
197
198
    /**
199
     * Word-wrap the message body to this number of chars.
200
     * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
201
     *
202
     * @see static::STD_LINE_LENGTH
203
     *
204
     * @var int
205
     */
206
    public $WordWrap = 0;
207
208
    /**
209
     * Which method to use to send mail.
210
     * Options: "mail", "sendmail", or "smtp".
211
     *
212
     * @var string
213
     */
214
    public $Mailer = 'mail';
215
216
    /**
217
     * The path to the sendmail program.
218
     *
219
     * @var string
220
     */
221
    public $Sendmail = '/usr/sbin/sendmail';
222
223
    /**
224
     * Whether mail() uses a fully sendmail-compatible MTA.
225
     * One which supports sendmail's "-oi -f" options.
226
     *
227
     * @var bool
228
     */
229
    public $UseSendmailOptions = true;
230
231
    /**
232
     * The email address that a reading confirmation should be sent to, also known as read receipt.
233
     *
234
     * @var string
235
     */
236
    public $ConfirmReadingTo = '';
237
238
    /**
239
     * The hostname to use in the Message-ID header and as default HELO string.
240
     * If empty, PHPMailer attempts to find one with, in order,
241
     * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
242
     * 'localhost.localdomain'.
243
     *
244
     * @see PHPMailer::$Helo
245
     *
246
     * @var string
247
     */
248
    public $Hostname = '';
249
250
    /**
251
     * An ID to be used in the Message-ID header.
252
     * If empty, a unique id will be generated.
253
     * You can set your own, but it must be in the format "<id@domain>",
254
     * as defined in RFC5322 section 3.6.4 or it will be ignored.
255
     *
256
     * @see https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4
257
     *
258
     * @var string
259
     */
260
    public $MessageID = '';
261
262
    /**
263
     * The message Date to be used in the Date header.
264
     * If empty, the current date will be added.
265
     *
266
     * @var string
267
     */
268
    public $MessageDate = '';
269
270
    /**
271
     * SMTP hosts.
272
     * Either a single hostname or multiple semicolon-delimited hostnames.
273
     * You can also specify a different port
274
     * for each host by using this format: [hostname:port]
275
     * (e.g. "smtp1.example.com:25;smtp2.example.com").
276
     * You can also specify encryption type, for example:
277
     * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
278
     * Hosts will be tried in order.
279
     *
280
     * @var string
281
     */
282
    public $Host = 'localhost';
283
284
    /**
285
     * The default SMTP server port.
286
     *
287
     * @var int
288
     */
289
    public $Port = 25;
290
291
    /**
292
     * The SMTP HELO/EHLO name used for the SMTP connection.
293
     * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
294
     * one with the same method described above for $Hostname.
295
     *
296
     * @see PHPMailer::$Hostname
297
     *
298
     * @var string
299
     */
300
    public $Helo = '';
301
302
    /**
303
     * What kind of encryption to use on the SMTP connection.
304
     * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
305
     *
306
     * @var string
307
     */
308
    public $SMTPSecure = '';
309
310
    /**
311
     * Whether to enable TLS encryption automatically if a server supports it,
312
     * even if `SMTPSecure` is not set to 'tls'.
313
     * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
314
     *
315
     * @var bool
316
     */
317
    public $SMTPAutoTLS = true;
318
319
    /**
320
     * Whether to use SMTP authentication.
321
     * Uses the Username and Password properties.
322
     *
323
     * @see PHPMailer::$Username
324
     * @see PHPMailer::$Password
325
     *
326
     * @var bool
327
     */
328
    public $SMTPAuth = false;
329
330
    /**
331
     * Options array passed to stream_context_create when connecting via SMTP.
332
     *
333
     * @var array
334
     */
335
    public $SMTPOptions = [];
336
337
    /**
338
     * SMTP username.
339
     *
340
     * @var string
341
     */
342
    public $Username = '';
343
344
    /**
345
     * SMTP password.
346
     *
347
     * @var string
348
     */
349
    public $Password = '';
350
351
    /**
352
     * SMTP authentication type. Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2.
353
     * If not specified, the first one from that list that the server supports will be selected.
354
     *
355
     * @var string
356
     */
357
    public $AuthType = '';
358
359
    /**
360
     * SMTP SMTPXClient command attributes
361
     *
362
     * @var array
363
     */
364
    protected $SMTPXClient = [];
365
366
    /**
367
     * An implementation of the PHPMailer OAuthTokenProvider interface.
368
     *
369
     * @var OAuthTokenProvider
370
     */
371
    protected $oauth;
372
373
    /**
374
     * The SMTP server timeout in seconds.
375
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
376
     *
377
     * @var int
378
     */
379
    public $Timeout = 300;
380
381
    /**
382
     * Comma separated list of DSN notifications
383
     * 'NEVER' under no circumstances a DSN must be returned to the sender.
384
     *         If you use NEVER all other notifications will be ignored.
385
     * 'SUCCESS' will notify you when your mail has arrived at its destination.
386
     * 'FAILURE' will arrive if an error occurred during delivery.
387
     * 'DELAY'   will notify you if there is an unusual delay in delivery, but the actual
388
     *           delivery's outcome (success or failure) is not yet decided.
389
     *
390
     * @see https://www.rfc-editor.org/rfc/rfc3461.html#section-4.1 for more information about NOTIFY
391
     */
392
    public $dsn = '';
393
394
    /**
395
     * SMTP class debug output mode.
396
     * Debug output level.
397
     * Options:
398
     * @see SMTP::DEBUG_OFF: No output
399
     * @see SMTP::DEBUG_CLIENT: Client messages
400
     * @see SMTP::DEBUG_SERVER: Client and server messages
401
     * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
402
     * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
403
     *
404
     * @see SMTP::$do_debug
405
     *
406
     * @var int
407
     */
408
    public $SMTPDebug = 0;
409
410
    /**
411
     * How to handle debug output.
412
     * Options:
413
     * * `echo` Output plain-text as-is, appropriate for CLI
414
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
415
     * * `error_log` Output to error log as configured in php.ini
416
     * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
417
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
418
     *
419
     * ```php
420
     * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
421
     * ```
422
     *
423
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
424
     * level output is used:
425
     *
426
     * ```php
427
     * $mail->Debugoutput = new myPsr3Logger;
428
     * ```
429
     *
430
     * @see SMTP::$Debugoutput
431
     *
432
     * @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...
433
     */
434
    public $Debugoutput = 'echo';
435
436
    /**
437
     * Whether to keep the SMTP connection open after each message.
438
     * If this is set to true then the connection will remain open after a send,
439
     * and closing the connection will require an explicit call to smtpClose().
440
     * It's a good idea to use this if you are sending multiple messages as it reduces overhead.
441
     * See the mailing list example for how to use it.
442
     *
443
     * @var bool
444
     */
445
    public $SMTPKeepAlive = false;
446
447
    /**
448
     * Whether to split multiple to addresses into multiple messages
449
     * or send them all in one message.
450
     * Only supported in `mail` and `sendmail` transports, not in SMTP.
451
     *
452
     * @var bool
453
     *
454
     * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
455
     */
456
    public $SingleTo = false;
457
458
    /**
459
     * Storage for addresses when SingleTo is enabled.
460
     *
461
     * @var array
462
     */
463
    protected $SingleToArray = [];
464
465
    /**
466
     * Whether to generate VERP addresses on send.
467
     * Only applicable when sending via SMTP.
468
     *
469
     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
470
     * @see https://www.postfix.org/VERP_README.html Postfix VERP info
471
     *
472
     * @var bool
473
     */
474
    public $do_verp = false;
475
476
    /**
477
     * Whether to allow sending messages with an empty body.
478
     *
479
     * @var bool
480
     */
481
    public $AllowEmpty = false;
482
483
    /**
484
     * DKIM selector.
485
     *
486
     * @var string
487
     */
488
    public $DKIM_selector = '';
489
490
    /**
491
     * DKIM Identity.
492
     * Usually the email address used as the source of the email.
493
     *
494
     * @var string
495
     */
496
    public $DKIM_identity = '';
497
498
    /**
499
     * DKIM passphrase.
500
     * Used if your key is encrypted.
501
     *
502
     * @var string
503
     */
504
    public $DKIM_passphrase = '';
505
506
    /**
507
     * DKIM signing domain name.
508
     *
509
     * @example 'example.com'
510
     *
511
     * @var string
512
     */
513
    public $DKIM_domain = '';
514
515
    /**
516
     * DKIM Copy header field values for diagnostic use.
517
     *
518
     * @var bool
519
     */
520
    public $DKIM_copyHeaderFields = true;
521
522
    /**
523
     * DKIM Extra signing headers.
524
     *
525
     * @example ['List-Unsubscribe', 'List-Help']
526
     *
527
     * @var array
528
     */
529
    public $DKIM_extraHeaders = [];
530
531
    /**
532
     * DKIM private key file path.
533
     *
534
     * @var string
535
     */
536
    public $DKIM_private = '';
537
538
    /**
539
     * DKIM private key string.
540
     *
541
     * If set, takes precedence over `$DKIM_private`.
542
     *
543
     * @var string
544
     */
545
    public $DKIM_private_string = '';
546
547
    /**
548
     * Callback Action function name.
549
     *
550
     * The function that handles the result of the send email action.
551
     * It is called out by send() for each email sent.
552
     *
553
     * Value can be any php callable: https://www.php.net/is_callable
554
     *
555
     * Parameters:
556
     *   bool $result           result of the send action
557
     *   array   $to            email addresses of the recipients
558
     *   array   $cc            cc email addresses
559
     *   array   $bcc           bcc email addresses
560
     *   string  $subject       the subject
561
     *   string  $body          the email body
562
     *   string  $from          email address of sender
563
     *   string  $extra         extra information of possible use
564
     *                          "smtp_transaction_id' => last smtp transaction id
565
     *
566
     * @var string
567
     */
568
    public $action_function = '';
569
570
    /**
571
     * What to put in the X-Mailer header.
572
     * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
573
     *
574
     * @var string|null
575
     */
576
    public $XMailer = '';
577
578
    /**
579
     * Which validator to use by default when validating email addresses.
580
     * May be a callable to inject your own validator, but there are several built-in validators.
581
     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
582
     *
583
     * @see PHPMailer::validateAddress()
584
     *
585
     * @var string|callable
586
     */
587
    public static $validator = 'php';
588
589
    /**
590
     * An instance of the SMTP sender class.
591
     *
592
     * @var SMTP
593
     */
594
    protected $smtp;
595
596
    /**
597
     * The array of 'to' names and addresses.
598
     *
599
     * @var array
600
     */
601
    protected $to = [];
602
603
    /**
604
     * The array of 'cc' names and addresses.
605
     *
606
     * @var array
607
     */
608
    protected $cc = [];
609
610
    /**
611
     * The array of 'bcc' names and addresses.
612
     *
613
     * @var array
614
     */
615
    protected $bcc = [];
616
617
    /**
618
     * The array of reply-to names and addresses.
619
     *
620
     * @var array
621
     */
622
    protected $ReplyTo = [];
623
624
    /**
625
     * An array of all kinds of addresses.
626
     * Includes all of $to, $cc, $bcc.
627
     *
628
     * @see PHPMailer::$to
629
     * @see PHPMailer::$cc
630
     * @see PHPMailer::$bcc
631
     *
632
     * @var array
633
     */
634
    protected $all_recipients = [];
635
636
    /**
637
     * An array of names and addresses queued for validation.
638
     * In send(), valid and non duplicate entries are moved to $all_recipients
639
     * and one of $to, $cc, or $bcc.
640
     * This array is used only for addresses with IDN.
641
     *
642
     * @see PHPMailer::$to
643
     * @see PHPMailer::$cc
644
     * @see PHPMailer::$bcc
645
     * @see PHPMailer::$all_recipients
646
     *
647
     * @var array
648
     */
649
    protected $RecipientsQueue = [];
650
651
    /**
652
     * An array of reply-to names and addresses queued for validation.
653
     * In send(), valid and non duplicate entries are moved to $ReplyTo.
654
     * This array is used only for addresses with IDN.
655
     *
656
     * @see PHPMailer::$ReplyTo
657
     *
658
     * @var array
659
     */
660
    protected $ReplyToQueue = [];
661
662
    /**
663
     * The array of attachments.
664
     *
665
     * @var array
666
     */
667
    protected $attachment = [];
668
669
    /**
670
     * The array of custom headers.
671
     *
672
     * @var array
673
     */
674
    protected $CustomHeader = [];
675
676
    /**
677
     * The most recent Message-ID (including angular brackets).
678
     *
679
     * @var string
680
     */
681
    protected $lastMessageID = '';
682
683
    /**
684
     * The message's MIME type.
685
     *
686
     * @var string
687
     */
688
    protected $message_type = '';
689
690
    /**
691
     * The array of MIME boundary strings.
692
     *
693
     * @var array
694
     */
695
    protected $boundary = [];
696
697
    /**
698
     * The array of available text strings for the current language.
699
     *
700
     * @var array
701
     */
702
    protected $language = [];
703
704
    /**
705
     * The number of errors encountered.
706
     *
707
     * @var int
708
     */
709
    protected $error_count = 0;
710
711
    /**
712
     * The S/MIME certificate file path.
713
     *
714
     * @var string
715
     */
716
    protected $sign_cert_file = '';
717
718
    /**
719
     * The S/MIME key file path.
720
     *
721
     * @var string
722
     */
723
    protected $sign_key_file = '';
724
725
    /**
726
     * The optional S/MIME extra certificates ("CA Chain") file path.
727
     *
728
     * @var string
729
     */
730
    protected $sign_extracerts_file = '';
731
732
    /**
733
     * The S/MIME password for the key.
734
     * Used only if the key is encrypted.
735
     *
736
     * @var string
737
     */
738
    protected $sign_key_pass = '';
739
740
    /**
741
     * Whether to throw exceptions for errors.
742
     *
743
     * @var bool
744
     */
745
    protected $exceptions = false;
746
747
    /**
748
     * Unique ID used for message ID and boundaries.
749
     *
750
     * @var string
751
     */
752
    protected $uniqueid = '';
753
754
    /**
755
     * The PHPMailer Version number.
756
     *
757
     * @var string
758
     */
759
    const VERSION = '6.9.3';
760
761
    /**
762
     * Error severity: message only, continue processing.
763
     *
764
     * @var int
765
     */
766
    const STOP_MESSAGE = 0;
767
768
    /**
769
     * Error severity: message, likely ok to continue processing.
770
     *
771
     * @var int
772
     */
773
    const STOP_CONTINUE = 1;
774
775
    /**
776
     * Error severity: message, plus full stop, critical error reached.
777
     *
778
     * @var int
779
     */
780
    const STOP_CRITICAL = 2;
781
782
    /**
783
     * The SMTP standard CRLF line break.
784
     * If you want to change line break format, change static::$LE, not this.
785
     */
786
    const CRLF = "\r\n";
787
788
    /**
789
     * "Folding White Space" a white space string used for line folding.
790
     */
791
    const FWS = ' ';
792
793
    /**
794
     * SMTP RFC standard line ending; Carriage Return, Line Feed.
795
     *
796
     * @var string
797
     */
798
    protected static $LE = self::CRLF;
799
800
    /**
801
     * The maximum line length supported by mail().
802
     *
803
     * Background: mail() will sometimes corrupt messages
804
     * with headers longer than 65 chars, see #818.
805
     *
806
     * @var int
807
     */
808
    const MAIL_MAX_LINE_LENGTH = 63;
809
810
    /**
811
     * The maximum line length allowed by RFC 2822 section 2.1.1.
812
     *
813
     * @var int
814
     */
815
    const MAX_LINE_LENGTH = 998;
816
817
    /**
818
     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
819
     * This length does NOT include the line break
820
     * 76 means that lines will be 77 or 78 chars depending on whether
821
     * the line break format is LF or CRLF; both are valid.
822
     *
823
     * @var int
824
     */
825
    const STD_LINE_LENGTH = 76;
826
827
    /**
828
     * Constructor.
829
     *
830
     * @param bool $exceptions Should we throw external exceptions?
831
     */
832
    public function __construct($exceptions = null)
833
    {
834
        if (null !== $exceptions) {
835
            $this->exceptions = (bool) $exceptions;
836
        }
837
        //Pick an appropriate debug output format automatically
838
        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
839
    }
840
841
    /**
842
     * Destructor.
843
     */
844
    public function __destruct()
845
    {
846
        //Close any open SMTP connection nicely
847
        $this->smtpClose();
848
    }
849
850
    /**
851
     * Call mail() in a safe_mode-aware fashion.
852
     * Also, unless sendmail_path points to sendmail (or something that
853
     * claims to be sendmail), don't pass params (not a perfect fix,
854
     * but it will do).
855
     *
856
     * @param string      $to      To
857
     * @param string      $subject Subject
858
     * @param string      $body    Message Body
859
     * @param string      $header  Additional Header(s)
860
     * @param string|null $params  Params
861
     *
862
     * @return bool
863
     */
864
    private function mailPassthru($to, $subject, $body, $header, $params)
865
    {
866
        //Check overloading of mail function to avoid double-encoding
867
        if ((int)ini_get('mbstring.func_overload') & 1) {
868
            $subject = $this->secureHeader($subject);
869
        } else {
870
            $subject = $this->encodeHeader($this->secureHeader($subject));
871
        }
872
        //Calling mail() with null params breaks
873
        $this->edebug('Sending with mail()');
874
        $this->edebug('Sendmail path: ' . ini_get('sendmail_path'));
875
        $this->edebug("Envelope sender: {$this->Sender}");
876
        $this->edebug("To: {$to}");
877
        $this->edebug("Subject: {$subject}");
878
        $this->edebug("Headers: {$header}");
879
        if (!$this->UseSendmailOptions || null === $params) {
880
            $result = @mail($to, $subject, $body, $header);
881
        } else {
882
            $this->edebug("Additional params: {$params}");
883
            $result = @mail($to, $subject, $body, $header, $params);
884
        }
885
        $this->edebug('Result: ' . ($result ? 'true' : 'false'));
886
        return $result;
887
    }
888
889
    /**
890
     * Output debugging info via a user-defined method.
891
     * Only generates output if debug output is enabled.
892
     *
893
     * @see PHPMailer::$Debugoutput
894
     * @see PHPMailer::$SMTPDebug
895
     *
896
     * @param string $str
897
     */
898
    protected function edebug($str)
899
    {
900
        if ($this->SMTPDebug <= 0) {
901
            return;
902
        }
903
        //Is this a PSR-3 logger?
904
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
905
            $this->Debugoutput->debug(rtrim($str, "\r\n"));
906
907
            return;
908
        }
909
        //Avoid clash with built-in function names
910
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
911
            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
912
913
            return;
914
        }
915
        switch ($this->Debugoutput) {
916
            case 'error_log':
917
                //Don't output, just log
918
                /** @noinspection ForgottenDebugOutputInspection */
919
                error_log($str);
920
                break;
921
            case 'html':
922
                //Cleans up output a bit for a better looking, HTML-safe output
923
                echo htmlentities(
924
                    preg_replace('/[\r\n]+/', '', $str),
925
                    ENT_QUOTES,
926
                    'UTF-8'
927
                ), "<br>\n";
928
                break;
929
            case 'echo':
930
            default:
931
                //Normalize line breaks
932
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
933
                echo gmdate('Y-m-d H:i:s'),
934
                "\t",
935
                    //Trim trailing space
936
                trim(
937
                    //Indent for readability, except for trailing break
938
                    str_replace(
939
                        "\n",
940
                        "\n                   \t                  ",
941
                        trim($str)
942
                    )
943
                ),
944
                "\n";
945
        }
946
    }
947
948
    /**
949
     * Sets message type to HTML or plain.
950
     *
951
     * @param bool $isHtml True for HTML mode
952
     */
953
    public function isHTML($isHtml = true)
954
    {
955
        if ($isHtml) {
956
            $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
957
        } else {
958
            $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
959
        }
960
    }
961
962
    /**
963
     * Send messages using SMTP.
964
     */
965
    public function isSMTP()
966
    {
967
        $this->Mailer = 'smtp';
968
    }
969
970
    /**
971
     * Send messages using PHP's mail() function.
972
     */
973
    public function isMail()
974
    {
975
        $this->Mailer = 'mail';
976
    }
977
978
    /**
979
     * Send messages using $Sendmail.
980
     */
981
    public function isSendmail()
982
    {
983
        $ini_sendmail_path = ini_get('sendmail_path');
984
985
        if (false === stripos($ini_sendmail_path, 'sendmail')) {
986
            $this->Sendmail = '/usr/sbin/sendmail';
987
        } else {
988
            $this->Sendmail = $ini_sendmail_path;
989
        }
990
        $this->Mailer = 'sendmail';
991
    }
992
993
    /**
994
     * Send messages using qmail.
995
     */
996
    public function isQmail()
997
    {
998
        $ini_sendmail_path = ini_get('sendmail_path');
999
1000
        if (false === stripos($ini_sendmail_path, 'qmail')) {
1001
            $this->Sendmail = '/var/qmail/bin/qmail-inject';
1002
        } else {
1003
            $this->Sendmail = $ini_sendmail_path;
1004
        }
1005
        $this->Mailer = 'qmail';
1006
    }
1007
1008
    /**
1009
     * Add a "To" address.
1010
     *
1011
     * @param string $address The email address to send to
1012
     * @param string $name
1013
     *
1014
     * @throws Exception
1015
     *
1016
     * @return bool true on success, false if address already used or invalid in some way
1017
     */
1018
    public function addAddress($address, $name = '')
1019
    {
1020
        return $this->addOrEnqueueAnAddress('to', $address, $name);
1021
    }
1022
1023
    /**
1024
     * Add a "CC" address.
1025
     *
1026
     * @param string $address The email address to send to
1027
     * @param string $name
1028
     *
1029
     * @throws Exception
1030
     *
1031
     * @return bool true on success, false if address already used or invalid in some way
1032
     */
1033
    public function addCC($address, $name = '')
1034
    {
1035
        return $this->addOrEnqueueAnAddress('cc', $address, $name);
1036
    }
1037
1038
    /**
1039
     * Add a "BCC" address.
1040
     *
1041
     * @param string $address The email address to send to
1042
     * @param string $name
1043
     *
1044
     * @throws Exception
1045
     *
1046
     * @return bool true on success, false if address already used or invalid in some way
1047
     */
1048
    public function addBCC($address, $name = '')
1049
    {
1050
        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
1051
    }
1052
1053
    /**
1054
     * Add a "Reply-To" address.
1055
     *
1056
     * @param string $address The email address to reply to
1057
     * @param string $name
1058
     *
1059
     * @throws Exception
1060
     *
1061
     * @return bool true on success, false if address already used or invalid in some way
1062
     */
1063
    public function addReplyTo($address, $name = '')
1064
    {
1065
        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
1066
    }
1067
1068
    /**
1069
     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
1070
     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
1071
     * be modified after calling this function), addition of such addresses is delayed until send().
1072
     * Addresses that have been added already return false, but do not throw exceptions.
1073
     *
1074
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'Reply-To'
1075
     * @param string $address The email address
1076
     * @param string $name    An optional username associated with the address
1077
     *
1078
     * @throws Exception
1079
     *
1080
     * @return bool true on success, false if address already used or invalid in some way
1081
     */
1082
    protected function addOrEnqueueAnAddress($kind, $address, $name)
1083
    {
1084
        $pos = false;
1085
        if ($address !== null) {
0 ignored issues
show
introduced by
The condition $address !== null is always true.
Loading history...
1086
            $address = trim($address);
1087
            $pos = strrpos($address, '@');
1088
        }
1089
        if (false === $pos) {
1090
            //At-sign is missing.
1091
            $error_message = sprintf(
1092
                '%s (%s): %s',
1093
                $this->lang('invalid_address'),
1094
                $kind,
1095
                $address
1096
            );
1097
            $this->setError($error_message);
1098
            $this->edebug($error_message);
1099
            if ($this->exceptions) {
1100
                throw new Exception($error_message);
1101
            }
1102
1103
            return false;
1104
        }
1105
        if ($name !== null && is_string($name)) {
0 ignored issues
show
introduced by
The condition is_string($name) is always true.
Loading history...
1106
            $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1107
        } else {
1108
            $name = '';
1109
        }
1110
        $params = [$kind, $address, $name];
1111
        //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
1112
        //Domain is assumed to be whatever is after the last @ symbol in the address
1113
        if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
1114
            if ('Reply-To' !== $kind) {
1115
                if (!array_key_exists($address, $this->RecipientsQueue)) {
1116
                    $this->RecipientsQueue[$address] = $params;
1117
1118
                    return true;
1119
                }
1120
            } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
1121
                $this->ReplyToQueue[$address] = $params;
1122
1123
                return true;
1124
            }
1125
1126
            return false;
1127
        }
1128
1129
        //Immediately add standard addresses without IDN.
1130
        return call_user_func_array([$this, 'addAnAddress'], $params);
1131
    }
1132
1133
    /**
1134
     * Set the boundaries to use for delimiting MIME parts.
1135
     * If you override this, ensure you set all 3 boundaries to unique values.
1136
     * The default boundaries include a "=_" sequence which cannot occur in quoted-printable bodies,
1137
     * as suggested by https://www.rfc-editor.org/rfc/rfc2045#section-6.7
1138
     *
1139
     * @return void
1140
     */
1141
    public function setBoundaries()
1142
    {
1143
        $this->uniqueid = $this->generateId();
1144
        $this->boundary[1] = 'b1=_' . $this->uniqueid;
1145
        $this->boundary[2] = 'b2=_' . $this->uniqueid;
1146
        $this->boundary[3] = 'b3=_' . $this->uniqueid;
1147
    }
1148
1149
    /**
1150
     * Add an address to one of the recipient arrays or to the ReplyTo array.
1151
     * Addresses that have been added already return false, but do not throw exceptions.
1152
     *
1153
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1154
     * @param string $address The email address to send, resp. to reply to
1155
     * @param string $name
1156
     *
1157
     * @throws Exception
1158
     *
1159
     * @return bool true on success, false if address already used or invalid in some way
1160
     */
1161
    protected function addAnAddress($kind, $address, $name = '')
1162
    {
1163
        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1164
            $error_message = sprintf(
1165
                '%s: %s',
1166
                $this->lang('Invalid recipient kind'),
1167
                $kind
1168
            );
1169
            $this->setError($error_message);
1170
            $this->edebug($error_message);
1171
            if ($this->exceptions) {
1172
                throw new Exception($error_message);
1173
            }
1174
1175
            return false;
1176
        }
1177
        if (!static::validateAddress($address)) {
1178
            $error_message = sprintf(
1179
                '%s (%s): %s',
1180
                $this->lang('invalid_address'),
1181
                $kind,
1182
                $address
1183
            );
1184
            $this->setError($error_message);
1185
            $this->edebug($error_message);
1186
            if ($this->exceptions) {
1187
                throw new Exception($error_message);
1188
            }
1189
1190
            return false;
1191
        }
1192
        if ('Reply-To' !== $kind) {
1193
            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
1194
                $this->{$kind}[] = [$address, $name];
1195
                $this->all_recipients[strtolower($address)] = true;
1196
1197
                return true;
1198
            }
1199
        } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
1200
            $this->ReplyTo[strtolower($address)] = [$address, $name];
1201
1202
            return true;
1203
        }
1204
1205
        return false;
1206
    }
1207
1208
    /**
1209
     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1210
     * of the form "display name <address>" into an array of name/address pairs.
1211
     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1212
     * Note that quotes in the name part are removed.
1213
     *
1214
     * @see https://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1215
     *
1216
     * @param string $addrstr The address list string
1217
     * @param bool   $useimap Whether to use the IMAP extension to parse the list
1218
     * @param string $charset The charset to use when decoding the address list string.
1219
     *
1220
     * @return array
1221
     */
1222
    public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591)
1223
    {
1224
        $addresses = [];
1225
        if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
1226
            //Use this built-in parser if it's available
1227
            $list = imap_rfc822_parse_adrlist($addrstr, '');
1228
            // Clear any potential IMAP errors to get rid of notices being thrown at end of script.
1229
            imap_errors();
1230
            foreach ($list as $address) {
1231
                if (
1232
                    '.SYNTAX-ERROR.' !== $address->host &&
1233
                    static::validateAddress($address->mailbox . '@' . $address->host)
1234
                ) {
1235
                    //Decode the name part if it's present and encoded
1236
                    if (
1237
                        property_exists($address, 'personal') &&
1238
                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
1239
                        defined('MB_CASE_UPPER') &&
1240
                        preg_match('/^=\?.*\?=$/s', $address->personal)
1241
                    ) {
1242
                        $origCharset = mb_internal_encoding();
1243
                        mb_internal_encoding($charset);
1244
                        //Undo any RFC2047-encoded spaces-as-underscores
1245
                        $address->personal = str_replace('_', '=20', $address->personal);
1246
                        //Decode the name
1247
                        $address->personal = mb_decode_mimeheader($address->personal);
1248
                        mb_internal_encoding($origCharset);
0 ignored issues
show
Bug introduced by
It seems like $origCharset can also be of type true; however, parameter $encoding of mb_internal_encoding() does only seem to accept null|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

1248
                        mb_internal_encoding(/** @scrutinizer ignore-type */ $origCharset);
Loading history...
1249
                    }
1250
1251
                    $addresses[] = [
1252
                        'name' => (property_exists($address, 'personal') ? $address->personal : ''),
1253
                        'address' => $address->mailbox . '@' . $address->host,
1254
                    ];
1255
                }
1256
            }
1257
        } else {
1258
            //Use this simpler parser
1259
            $list = explode(',', $addrstr);
1260
            foreach ($list as $address) {
1261
                $address = trim($address);
1262
                //Is there a separate name part?
1263
                if (strpos($address, '<') === false) {
1264
                    //No separate name, just use the whole thing
1265
                    if (static::validateAddress($address)) {
1266
                        $addresses[] = [
1267
                            'name' => '',
1268
                            'address' => $address,
1269
                        ];
1270
                    }
1271
                } else {
1272
                    list($name, $email) = explode('<', $address);
1273
                    $email = trim(str_replace('>', '', $email));
1274
                    $name = trim($name);
1275
                    if (static::validateAddress($email)) {
1276
                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
1277
                        //If this name is encoded, decode it
1278
                        if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) {
1279
                            $origCharset = mb_internal_encoding();
1280
                            mb_internal_encoding($charset);
1281
                            //Undo any RFC2047-encoded spaces-as-underscores
1282
                            $name = str_replace('_', '=20', $name);
1283
                            //Decode the name
1284
                            $name = mb_decode_mimeheader($name);
1285
                            mb_internal_encoding($origCharset);
1286
                        }
1287
                        $addresses[] = [
1288
                            //Remove any surrounding quotes and spaces from the name
1289
                            'name' => trim($name, '\'" '),
1290
                            'address' => $email,
1291
                        ];
1292
                    }
1293
                }
1294
            }
1295
        }
1296
1297
        return $addresses;
1298
    }
1299
1300
    /**
1301
     * Set the From and FromName properties.
1302
     *
1303
     * @param string $address
1304
     * @param string $name
1305
     * @param bool   $auto    Whether to also set the Sender address, defaults to true
1306
     *
1307
     * @throws Exception
1308
     *
1309
     * @return bool
1310
     */
1311
    public function setFrom($address, $name = '', $auto = true)
1312
    {
1313
        $address = trim((string)$address);
1314
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1315
        //Don't validate now addresses with IDN. Will be done in send().
1316
        $pos = strrpos($address, '@');
1317
        if (
1318
            (false === $pos)
1319
            || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
1320
            && !static::validateAddress($address))
1321
        ) {
1322
            $error_message = sprintf(
1323
                '%s (From): %s',
1324
                $this->lang('invalid_address'),
1325
                $address
1326
            );
1327
            $this->setError($error_message);
1328
            $this->edebug($error_message);
1329
            if ($this->exceptions) {
1330
                throw new Exception($error_message);
1331
            }
1332
1333
            return false;
1334
        }
1335
        $this->From = $address;
1336
        $this->FromName = $name;
1337
        if ($auto && empty($this->Sender)) {
1338
            $this->Sender = $address;
1339
        }
1340
1341
        return true;
1342
    }
1343
1344
    /**
1345
     * Return the Message-ID header of the last email.
1346
     * Technically this is the value from the last time the headers were created,
1347
     * but it's also the message ID of the last sent message except in
1348
     * pathological cases.
1349
     *
1350
     * @return string
1351
     */
1352
    public function getLastMessageID()
1353
    {
1354
        return $this->lastMessageID;
1355
    }
1356
1357
    /**
1358
     * Check that a string looks like an email address.
1359
     * Validation patterns supported:
1360
     * * `auto` Pick best pattern automatically;
1361
     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
1362
     * * `pcre` Use old PCRE implementation;
1363
     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
1364
     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
1365
     * * `noregex` Don't use a regex: super fast, really dumb.
1366
     * Alternatively you may pass in a callable to inject your own validator, for example:
1367
     *
1368
     * ```php
1369
     * PHPMailer::validateAddress('[email protected]', function($address) {
1370
     *     return (strpos($address, '@') !== false);
1371
     * });
1372
     * ```
1373
     *
1374
     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
1375
     *
1376
     * @param string          $address       The email address to check
1377
     * @param string|callable $patternselect Which pattern to use
1378
     *
1379
     * @return bool
1380
     */
1381
    public static function validateAddress($address, $patternselect = null)
1382
    {
1383
        if (null === $patternselect) {
1384
            $patternselect = static::$validator;
1385
        }
1386
        //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603
1387
        if (is_callable($patternselect) && !is_string($patternselect)) {
1388
            return call_user_func($patternselect, $address);
1389
        }
1390
        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
1391
        if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
1392
            return false;
1393
        }
1394
        switch ($patternselect) {
1395
            case 'pcre': //Kept for BC
1396
            case 'pcre8':
1397
                /*
1398
                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
1399
                 * is based.
1400
                 * In addition to the addresses allowed by filter_var, also permits:
1401
                 *  * dotless domains: `a@b`
1402
                 *  * comments: `1234 @ local(blah) .machine .example`
1403
                 *  * quoted elements: `'"test blah"@example.org'`
1404
                 *  * numeric TLDs: `[email protected]`
1405
                 *  * unbracketed IPv4 literals: `[email protected]`
1406
                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'
1407
                 * Not all of these will necessarily work for sending!
1408
                 *
1409
                 * @copyright 2009-2010 Michael Rushton
1410
                 * Feel free to use and redistribute this code. But please keep this copyright notice.
1411
                 */
1412
                return (bool) preg_match(
1413
                    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
1414
                    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
1415
                    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
1416
                    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
1417
                    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
1418
                    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
1419
                    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
1420
                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
1421
                    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
1422
                    $address
1423
                );
1424
            case 'html5':
1425
                /*
1426
                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
1427
                 *
1428
                 * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
1429
                 */
1430
                return (bool) preg_match(
1431
                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
1432
                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
1433
                    $address
1434
                );
1435
            case 'php':
1436
            default:
1437
                return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
1438
        }
1439
    }
1440
1441
    /**
1442
     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
1443
     * `intl` and `mbstring` PHP extensions.
1444
     *
1445
     * @return bool `true` if required functions for IDN support are present
1446
     */
1447
    public static function idnSupported()
1448
    {
1449
        return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
1450
    }
1451
1452
    /**
1453
     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
1454
     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
1455
     * This function silently returns unmodified address if:
1456
     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
1457
     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
1458
     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).
1459
     *
1460
     * @see PHPMailer::$CharSet
1461
     *
1462
     * @param string $address The email address to convert
1463
     *
1464
     * @return string The encoded address in ASCII form
1465
     */
1466
    public function punyencodeAddress($address)
1467
    {
1468
        //Verify we have required functions, CharSet, and at-sign.
1469
        $pos = strrpos($address, '@');
1470
        if (
1471
            !empty($this->CharSet) &&
1472
            false !== $pos &&
1473
            static::idnSupported()
1474
        ) {
1475
            $domain = substr($address, ++$pos);
1476
            //Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
1477
            if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
1478
                //Convert the domain from whatever charset it's in to UTF-8
1479
                $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);
1480
                //Ignore IDE complaints about this line - method signature changed in PHP 5.4
1481
                $errorcode = 0;
1482
                if (defined('INTL_IDNA_VARIANT_UTS46')) {
1483
                    //Use the current punycode standard (appeared in PHP 7.2)
1484
                    $punycode = idn_to_ascii(
1485
                        $domain,
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

1485
                        /** @scrutinizer ignore-type */ $domain,
Loading history...
1486
                        \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI |
1487
                            \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII,
1488
                        \INTL_IDNA_VARIANT_UTS46
1489
                    );
1490
                } elseif (defined('INTL_IDNA_VARIANT_2003')) {
1491
                    //Fall back to this old, deprecated/removed encoding
1492
                    $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

1492
                    $punycode = idn_to_ascii($domain, $errorcode, /** @scrutinizer ignore-deprecated */ \INTL_IDNA_VARIANT_2003);
Loading history...
1493
                } else {
1494
                    //Fall back to a default we don't know about
1495
                    $punycode = idn_to_ascii($domain, $errorcode);
1496
                }
1497
                if (false !== $punycode) {
1498
                    return substr($address, 0, $pos) . $punycode;
1499
                }
1500
            }
1501
        }
1502
1503
        return $address;
1504
    }
1505
1506
    /**
1507
     * Create a message and send it.
1508
     * Uses the sending method specified by $Mailer.
1509
     *
1510
     * @throws Exception
1511
     *
1512
     * @return bool false on error - See the ErrorInfo property for details of the error
1513
     */
1514
    public function send()
1515
    {
1516
        try {
1517
            if (!$this->preSend()) {
1518
                return false;
1519
            }
1520
1521
            return $this->postSend();
1522
        } catch (Exception $exc) {
1523
            $this->mailHeader = '';
1524
            $this->setError($exc->getMessage());
1525
            if ($this->exceptions) {
1526
                throw $exc;
1527
            }
1528
1529
            return false;
1530
        }
1531
    }
1532
1533
    /**
1534
     * Prepare a message for sending.
1535
     *
1536
     * @throws Exception
1537
     *
1538
     * @return bool
1539
     */
1540
    public function preSend()
1541
    {
1542
        if (
1543
            'smtp' === $this->Mailer
1544
            || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
1545
        ) {
1546
            //SMTP mandates RFC-compliant line endings
1547
            //and it's also used with mail() on Windows
1548
            static::setLE(self::CRLF);
1549
        } else {
1550
            //Maintain backward compatibility with legacy Linux command line mailers
1551
            static::setLE(PHP_EOL);
1552
        }
1553
        //Check for buggy PHP versions that add a header with an incorrect line break
1554
        if (
1555
            'mail' === $this->Mailer
1556
            && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
1557
                || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
1558
            && ini_get('mail.add_x_header') === '1'
1559
            && stripos(PHP_OS, 'WIN') === 0
1560
        ) {
1561
            trigger_error($this->lang('buggy_php'), E_USER_WARNING);
1562
        }
1563
1564
        try {
1565
            $this->error_count = 0; //Reset errors
1566
            $this->mailHeader = '';
1567
1568
            //Dequeue recipient and Reply-To addresses with IDN
1569
            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
1570
                $params[1] = $this->punyencodeAddress($params[1]);
1571
                call_user_func_array([$this, 'addAnAddress'], $params);
1572
            }
1573
            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
1574
                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
1575
            }
1576
1577
            //Validate From, Sender, and ConfirmReadingTo addresses
1578
            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
1579
                if ($this->{$address_kind} === null) {
1580
                    $this->{$address_kind} = '';
1581
                    continue;
1582
                }
1583
                $this->{$address_kind} = trim($this->{$address_kind});
1584
                if (empty($this->{$address_kind})) {
1585
                    continue;
1586
                }
1587
                $this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind});
1588
                if (!static::validateAddress($this->{$address_kind})) {
1589
                    $error_message = sprintf(
1590
                        '%s (%s): %s',
1591
                        $this->lang('invalid_address'),
1592
                        $address_kind,
1593
                        $this->{$address_kind}
1594
                    );
1595
                    $this->setError($error_message);
1596
                    $this->edebug($error_message);
1597
                    if ($this->exceptions) {
1598
                        throw new Exception($error_message);
1599
                    }
1600
1601
                    return false;
1602
                }
1603
            }
1604
1605
            //Set whether the message is multipart/alternative
1606
            if ($this->alternativeExists()) {
1607
                $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
1608
            }
1609
1610
            $this->setMessageType();
1611
            //Refuse to send an empty message unless we are specifically allowing it
1612
            if (!$this->AllowEmpty && empty($this->Body)) {
1613
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
1614
            }
1615
1616
            //Trim subject consistently
1617
            $this->Subject = trim($this->Subject);
1618
            //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
1619
            $this->MIMEHeader = '';
1620
            $this->MIMEBody = $this->createBody();
1621
            //createBody may have added some headers, so retain them
1622
            $tempheaders = $this->MIMEHeader;
1623
            $this->MIMEHeader = $this->createHeader();
1624
            $this->MIMEHeader .= $tempheaders;
1625
1626
            //To capture the complete message when using mail(), create
1627
            //an extra header list which createHeader() doesn't fold in
1628
            if ('mail' === $this->Mailer) {
1629
                if (count($this->to) > 0) {
1630
                    $this->mailHeader .= $this->addrAppend('To', $this->to);
1631
                } else {
1632
                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
1633
                }
1634
                $this->mailHeader .= $this->headerLine(
1635
                    'Subject',
1636
                    $this->encodeHeader($this->secureHeader($this->Subject))
1637
                );
1638
            }
1639
1640
            //Sign with DKIM if enabled
1641
            if (
1642
                !empty($this->DKIM_domain)
1643
                && !empty($this->DKIM_selector)
1644
                && (!empty($this->DKIM_private_string)
1645
                    || (!empty($this->DKIM_private)
1646
                        && static::isPermittedPath($this->DKIM_private)
1647
                        && file_exists($this->DKIM_private)
1648
                    )
1649
                )
1650
            ) {
1651
                $header_dkim = $this->DKIM_Add(
1652
                    $this->MIMEHeader . $this->mailHeader,
1653
                    $this->encodeHeader($this->secureHeader($this->Subject)),
1654
                    $this->MIMEBody
1655
                );
1656
                $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .
1657
                    static::normalizeBreaks($header_dkim) . static::$LE;
1658
            }
1659
1660
            return true;
1661
        } catch (Exception $exc) {
1662
            $this->setError($exc->getMessage());
1663
            if ($this->exceptions) {
1664
                throw $exc;
1665
            }
1666
1667
            return false;
1668
        }
1669
    }
1670
1671
    /**
1672
     * Actually send a message via the selected mechanism.
1673
     *
1674
     * @throws Exception
1675
     *
1676
     * @return bool
1677
     */
1678
    public function postSend()
1679
    {
1680
        try {
1681
            //Choose the mailer and send through it
1682
            switch ($this->Mailer) {
1683
                case 'sendmail':
1684
                case 'qmail':
1685
                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
1686
                case 'smtp':
1687
                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
1688
                case 'mail':
1689
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1690
                default:
1691
                    $sendMethod = $this->Mailer . 'Send';
1692
                    if (method_exists($this, $sendMethod)) {
1693
                        return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody);
1694
                    }
1695
1696
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1697
            }
1698
        } catch (Exception $exc) {
1699
            $this->setError($exc->getMessage());
1700
            $this->edebug($exc->getMessage());
1701
            if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) {
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...
1702
                $this->smtp->reset();
1703
            }
1704
            if ($this->exceptions) {
1705
                throw $exc;
1706
            }
1707
        }
1708
1709
        return false;
1710
    }
1711
1712
    /**
1713
     * Send mail using the $Sendmail program.
1714
     *
1715
     * @see PHPMailer::$Sendmail
1716
     *
1717
     * @param string $header The message headers
1718
     * @param string $body   The message body
1719
     *
1720
     * @throws Exception
1721
     *
1722
     * @return bool
1723
     */
1724
    protected function sendmailSend($header, $body)
1725
    {
1726
        if ($this->Mailer === 'qmail') {
1727
            $this->edebug('Sending with qmail');
1728
        } else {
1729
            $this->edebug('Sending with sendmail');
1730
        }
1731
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1732
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1733
        //A space after `-f` is optional, but there is a long history of its presence
1734
        //causing problems, so we don't use one
1735
        //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1736
        //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html
1737
        //Example problem: https://www.drupal.org/node/1057954
1738
1739
        //PHP 5.6 workaround
1740
        $sendmail_from_value = ini_get('sendmail_from');
1741
        if (empty($this->Sender) && !empty($sendmail_from_value)) {
1742
            //PHP config has a sender address we can use
1743
            $this->Sender = ini_get('sendmail_from');
1744
        }
1745
        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1746
        if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
1747
            if ($this->Mailer === 'qmail') {
1748
                $sendmailFmt = '%s -f%s';
1749
            } else {
1750
                $sendmailFmt = '%s -oi -f%s -t';
1751
            }
1752
        } else {
1753
            //allow sendmail to choose a default envelope sender. It may
1754
            //seem preferable to force it to use the From header as with
1755
            //SMTP, but that introduces new problems (see
1756
            //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and
1757
            //it has historically worked this way.
1758
            $sendmailFmt = '%s -oi -t';
1759
        }
1760
1761
        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
1762
        $this->edebug('Sendmail path: ' . $this->Sendmail);
1763
        $this->edebug('Sendmail command: ' . $sendmail);
1764
        $this->edebug('Envelope sender: ' . $this->Sender);
1765
        $this->edebug("Headers: {$header}");
1766
1767
        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

1767
        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...
1768
            foreach ($this->SingleToArray as $toAddr) {
1769
                $mail = @popen($sendmail, 'w');
1770
                if (!$mail) {
1771
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1772
                }
1773
                $this->edebug("To: {$toAddr}");
1774
                fwrite($mail, 'To: ' . $toAddr . "\n");
1775
                fwrite($mail, $header);
1776
                fwrite($mail, $body);
1777
                $result = pclose($mail);
1778
                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
1779
                $this->doCallback(
1780
                    ($result === 0),
1781
                    [[$addrinfo['address'], $addrinfo['name']]],
1782
                    $this->cc,
1783
                    $this->bcc,
1784
                    $this->Subject,
1785
                    $body,
1786
                    $this->From,
1787
                    []
1788
                );
1789
                $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
1790
                if (0 !== $result) {
1791
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1792
                }
1793
            }
1794
        } else {
1795
            $mail = @popen($sendmail, 'w');
1796
            if (!$mail) {
0 ignored issues
show
introduced by
$mail is of type false|resource, thus it always evaluated to false.
Loading history...
1797
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1798
            }
1799
            fwrite($mail, $header);
1800
            fwrite($mail, $body);
1801
            $result = pclose($mail);
1802
            $this->doCallback(
1803
                ($result === 0),
1804
                $this->to,
1805
                $this->cc,
1806
                $this->bcc,
1807
                $this->Subject,
1808
                $body,
1809
                $this->From,
1810
                []
1811
            );
1812
            $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
1813
            if (0 !== $result) {
1814
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1815
            }
1816
        }
1817
1818
        return true;
1819
    }
1820
1821
    /**
1822
     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
1823
     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
1824
     *
1825
     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
1826
     *
1827
     * @param string $string The string to be validated
1828
     *
1829
     * @return bool
1830
     */
1831
    protected static function isShellSafe($string)
1832
    {
1833
        //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg,
1834
        //but some hosting providers disable it, creating a security problem that we don't want to have to deal with,
1835
        //so we don't.
1836
        if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) {
1837
            return false;
1838
        }
1839
1840
        if (
1841
            escapeshellcmd($string) !== $string
1842
            || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
1843
        ) {
1844
            return false;
1845
        }
1846
1847
        $length = strlen($string);
1848
1849
        for ($i = 0; $i < $length; ++$i) {
1850
            $c = $string[$i];
1851
1852
            //All other characters have a special meaning in at least one common shell, including = and +.
1853
            //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
1854
            //Note that this does permit non-Latin alphanumeric characters based on the current locale.
1855
            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
1856
                return false;
1857
            }
1858
        }
1859
1860
        return true;
1861
    }
1862
1863
    /**
1864
     * Check whether a file path is of a permitted type.
1865
     * Used to reject URLs and phar files from functions that access local file paths,
1866
     * such as addAttachment.
1867
     *
1868
     * @param string $path A relative or absolute path to a file
1869
     *
1870
     * @return bool
1871
     */
1872
    protected static function isPermittedPath($path)
1873
    {
1874
        //Matches scheme definition from https://www.rfc-editor.org/rfc/rfc3986#section-3.1
1875
        return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path);
1876
    }
1877
1878
    /**
1879
     * Check whether a file path is safe, accessible, and readable.
1880
     *
1881
     * @param string $path A relative or absolute path to a file
1882
     *
1883
     * @return bool
1884
     */
1885
    protected static function fileIsAccessible($path)
1886
    {
1887
        if (!static::isPermittedPath($path)) {
1888
            return false;
1889
        }
1890
        $readable = is_file($path);
1891
        //If not a UNC path (expected to start with \\), check read permission, see #2069
1892
        if (strpos($path, '\\\\') !== 0) {
1893
            $readable = $readable && is_readable($path);
1894
        }
1895
        return  $readable;
1896
    }
1897
1898
    /**
1899
     * Send mail using the PHP mail() function.
1900
     *
1901
     * @see https://www.php.net/manual/en/book.mail.php
1902
     *
1903
     * @param string $header The message headers
1904
     * @param string $body   The message body
1905
     *
1906
     * @throws Exception
1907
     *
1908
     * @return bool
1909
     */
1910
    protected function mailSend($header, $body)
1911
    {
1912
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1913
1914
        $toArr = [];
1915
        foreach ($this->to as $toaddr) {
1916
            $toArr[] = $this->addrFormat($toaddr);
1917
        }
1918
        $to = trim(implode(', ', $toArr));
1919
1920
        //If there are no To-addresses (e.g. when sending only to BCC-addresses)
1921
        //the following should be added to get a correct DKIM-signature.
1922
        //Compare with $this->preSend()
1923
        if ($to === '') {
1924
            $to = 'undisclosed-recipients:;';
1925
        }
1926
1927
        $params = null;
1928
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1929
        //A space after `-f` is optional, but there is a long history of its presence
1930
        //causing problems, so we don't use one
1931
        //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1932
        //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html
1933
        //Example problem: https://www.drupal.org/node/1057954
1934
        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1935
1936
        //PHP 5.6 workaround
1937
        $sendmail_from_value = ini_get('sendmail_from');
1938
        if (empty($this->Sender) && !empty($sendmail_from_value)) {
1939
            //PHP config has a sender address we can use
1940
            $this->Sender = ini_get('sendmail_from');
1941
        }
1942
        if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
1943
            if (self::isShellSafe($this->Sender)) {
1944
                $params = sprintf('-f%s', $this->Sender);
1945
            }
1946
            $old_from = ini_get('sendmail_from');
1947
            ini_set('sendmail_from', $this->Sender);
1948
        }
1949
        $result = false;
1950
        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

1950
        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...
1951
            foreach ($toArr as $toAddr) {
1952
                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
1953
                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
1954
                $this->doCallback(
1955
                    $result,
1956
                    [[$addrinfo['address'], $addrinfo['name']]],
1957
                    $this->cc,
1958
                    $this->bcc,
1959
                    $this->Subject,
1960
                    $body,
1961
                    $this->From,
1962
                    []
1963
                );
1964
            }
1965
        } else {
1966
            $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
1967
            $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
1968
        }
1969
        if (isset($old_from)) {
1970
            ini_set('sendmail_from', $old_from);
1971
        }
1972
        if (!$result) {
1973
            throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
1974
        }
1975
1976
        return true;
1977
    }
1978
1979
    /**
1980
     * Get an instance to use for SMTP operations.
1981
     * Override this function to load your own SMTP implementation,
1982
     * or set one with setSMTPInstance.
1983
     *
1984
     * @return SMTP
1985
     */
1986
    public function getSMTPInstance()
1987
    {
1988
        if (!is_object($this->smtp)) {
1989
            $this->smtp = new SMTP();
1990
        }
1991
1992
        return $this->smtp;
1993
    }
1994
1995
    /**
1996
     * Provide an instance to use for SMTP operations.
1997
     *
1998
     * @return SMTP
1999
     */
2000
    public function setSMTPInstance(SMTP $smtp)
2001
    {
2002
        $this->smtp = $smtp;
2003
2004
        return $this->smtp;
2005
    }
2006
2007
    /**
2008
     * Provide SMTP XCLIENT attributes
2009
     *
2010
     * @param string $name  Attribute name
2011
     * @param ?string $value Attribute value
2012
     *
2013
     * @return bool
2014
     */
2015
    public function setSMTPXclientAttribute($name, $value)
2016
    {
2017
        if (!in_array($name, SMTP::$xclient_allowed_attributes)) {
2018
            return false;
2019
        }
2020
        if (isset($this->SMTPXClient[$name]) && $value === null) {
2021
            unset($this->SMTPXClient[$name]);
2022
        } elseif ($value !== null) {
2023
            $this->SMTPXClient[$name] = $value;
2024
        }
2025
2026
        return true;
2027
    }
2028
2029
    /**
2030
     * Get SMTP XCLIENT attributes
2031
     *
2032
     * @return array
2033
     */
2034
    public function getSMTPXclientAttributes()
2035
    {
2036
        return $this->SMTPXClient;
2037
    }
2038
2039
    /**
2040
     * Send mail via SMTP.
2041
     * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
2042
     *
2043
     * @see PHPMailer::setSMTPInstance() to use a different class.
2044
     *
2045
     * @uses \PHPMailer\PHPMailer\SMTP
2046
     *
2047
     * @param string $header The message headers
2048
     * @param string $body   The message body
2049
     *
2050
     * @throws Exception
2051
     *
2052
     * @return bool
2053
     */
2054
    protected function smtpSend($header, $body)
2055
    {
2056
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
2057
        $bad_rcpt = [];
2058
        if (!$this->smtpConnect($this->SMTPOptions)) {
2059
            throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
2060
        }
2061
        //Sender already validated in preSend()
2062
        if ('' === $this->Sender) {
2063
            $smtp_from = $this->From;
2064
        } else {
2065
            $smtp_from = $this->Sender;
2066
        }
2067
        if (count($this->SMTPXClient)) {
2068
            $this->smtp->xclient($this->SMTPXClient);
2069
        }
2070
        if (!$this->smtp->mail($smtp_from)) {
2071
            $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
2072
            throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
2073
        }
2074
2075
        $callbacks = [];
2076
        //Attempt to send to all recipients
2077
        foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
2078
            foreach ($togroup as $to) {
2079
                if (!$this->smtp->recipient($to[0], $this->dsn)) {
2080
                    $error = $this->smtp->getError();
2081
                    $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
2082
                    $isSent = false;
2083
                } else {
2084
                    $isSent = true;
2085
                }
2086
2087
                $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]];
2088
            }
2089
        }
2090
2091
        //Only send the DATA command if we have viable recipients
2092
        if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
2093
            throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
2094
        }
2095
2096
        $smtp_transaction_id = $this->smtp->getLastTransactionID();
2097
2098
        if ($this->SMTPKeepAlive) {
2099
            $this->smtp->reset();
2100
        } else {
2101
            $this->smtp->quit();
2102
            $this->smtp->close();
2103
        }
2104
2105
        foreach ($callbacks as $cb) {
2106
            $this->doCallback(
2107
                $cb['issent'],
2108
                [[$cb['to'], $cb['name']]],
2109
                [],
2110
                [],
2111
                $this->Subject,
2112
                $body,
2113
                $this->From,
2114
                ['smtp_transaction_id' => $smtp_transaction_id]
2115
            );
2116
        }
2117
2118
        //Create error message for any bad addresses
2119
        if (count($bad_rcpt) > 0) {
2120
            $errstr = '';
2121
            foreach ($bad_rcpt as $bad) {
2122
                $errstr .= $bad['to'] . ': ' . $bad['error'];
2123
            }
2124
            throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE);
2125
        }
2126
2127
        return true;
2128
    }
2129
2130
    /**
2131
     * Initiate a connection to an SMTP server.
2132
     * Returns false if the operation failed.
2133
     *
2134
     * @param array $options An array of options compatible with stream_context_create()
2135
     *
2136
     * @throws Exception
2137
     *
2138
     * @uses \PHPMailer\PHPMailer\SMTP
2139
     *
2140
     * @return bool
2141
     */
2142
    public function smtpConnect($options = null)
2143
    {
2144
        if (null === $this->smtp) {
2145
            $this->smtp = $this->getSMTPInstance();
2146
        }
2147
2148
        //If no options are provided, use whatever is set in the instance
2149
        if (null === $options) {
2150
            $options = $this->SMTPOptions;
2151
        }
2152
2153
        //Already connected?
2154
        if ($this->smtp->connected()) {
2155
            return true;
2156
        }
2157
2158
        $this->smtp->setTimeout($this->Timeout);
2159
        $this->smtp->setDebugLevel($this->SMTPDebug);
2160
        $this->smtp->setDebugOutput($this->Debugoutput);
2161
        $this->smtp->setVerp($this->do_verp);
2162
        if ($this->Host === null) {
2163
            $this->Host = 'localhost';
2164
        }
2165
        $hosts = explode(';', $this->Host);
2166
        $lastexception = null;
2167
2168
        foreach ($hosts as $hostentry) {
2169
            $hostinfo = [];
2170
            if (
2171
                !preg_match(
2172
                    '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
2173
                    trim($hostentry),
2174
                    $hostinfo
2175
                )
2176
            ) {
2177
                $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
2178
                //Not a valid host entry
2179
                continue;
2180
            }
2181
            //$hostinfo[1]: optional ssl or tls prefix
2182
            //$hostinfo[2]: the hostname
2183
            //$hostinfo[3]: optional port number
2184
            //The host string prefix can temporarily override the current setting for SMTPSecure
2185
            //If it's not specified, the default value is used
2186
2187
            //Check the host name is a valid name or IP address before trying to use it
2188
            if (!static::isValidHost($hostinfo[2])) {
2189
                $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
2190
                continue;
2191
            }
2192
            $prefix = '';
2193
            $secure = $this->SMTPSecure;
2194
            $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
2195
            if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
2196
                $prefix = 'ssl://';
2197
                $tls = false; //Can't have SSL and TLS at the same time
2198
                $secure = static::ENCRYPTION_SMTPS;
2199
            } elseif ('tls' === $hostinfo[1]) {
2200
                $tls = true;
2201
                //TLS doesn't use a prefix
2202
                $secure = static::ENCRYPTION_STARTTLS;
2203
            }
2204
            //Do we need the OpenSSL extension?
2205
            $sslext = defined('OPENSSL_ALGO_SHA256');
2206
            if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
2207
                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
2208
                if (!$sslext) {
2209
                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
2210
                }
2211
            }
2212
            $host = $hostinfo[2];
2213
            $port = $this->Port;
2214
            if (
2215
                array_key_exists(3, $hostinfo) &&
2216
                is_numeric($hostinfo[3]) &&
2217
                $hostinfo[3] > 0 &&
2218
                $hostinfo[3] < 65536
2219
            ) {
2220
                $port = (int) $hostinfo[3];
2221
            }
2222
            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
2223
                try {
2224
                    if ($this->Helo) {
2225
                        $hello = $this->Helo;
2226
                    } else {
2227
                        $hello = $this->serverHostname();
2228
                    }
2229
                    $this->smtp->hello($hello);
2230
                    //Automatically enable TLS encryption if:
2231
                    //* it's not disabled
2232
                    //* we are not connecting to localhost
2233
                    //* we have openssl extension
2234
                    //* we are not already using SSL
2235
                    //* the server offers STARTTLS
2236
                    if (
2237
                        $this->SMTPAutoTLS &&
2238
                        $this->Host !== 'localhost' &&
2239
                        $sslext &&
2240
                        $secure !== 'ssl' &&
2241
                        $this->smtp->getServerExt('STARTTLS')
2242
                    ) {
2243
                        $tls = true;
2244
                    }
2245
                    if ($tls) {
2246
                        if (!$this->smtp->startTLS()) {
2247
                            $message = $this->getSmtpErrorMessage('connect_host');
2248
                            throw new Exception($message);
2249
                        }
2250
                        //We must resend EHLO after TLS negotiation
2251
                        $this->smtp->hello($hello);
2252
                    }
2253
                    if (
2254
                        $this->SMTPAuth && !$this->smtp->authenticate(
2255
                            $this->Username,
2256
                            $this->Password,
2257
                            $this->AuthType,
2258
                            $this->oauth
2259
                        )
2260
                    ) {
2261
                        throw new Exception($this->lang('authenticate'));
2262
                    }
2263
2264
                    return true;
2265
                } catch (Exception $exc) {
2266
                    $lastexception = $exc;
2267
                    $this->edebug($exc->getMessage());
2268
                    //We must have connected, but then failed TLS or Auth, so close connection nicely
2269
                    $this->smtp->quit();
2270
                }
2271
            }
2272
        }
2273
        //If we get here, all connection attempts have failed, so close connection hard
2274
        $this->smtp->close();
2275
        //As we've caught all exceptions, just report whatever the last one was
2276
        if ($this->exceptions && null !== $lastexception) {
2277
            throw $lastexception;
2278
        }
2279
        if ($this->exceptions) {
2280
            // no exception was thrown, likely $this->smtp->connect() failed
2281
            $message = $this->getSmtpErrorMessage('connect_host');
2282
            throw new Exception($message);
2283
        }
2284
2285
        return false;
2286
    }
2287
2288
    /**
2289
     * Close the active SMTP session if one exists.
2290
     */
2291
    public function smtpClose()
2292
    {
2293
        if ((null !== $this->smtp) && $this->smtp->connected()) {
2294
            $this->smtp->quit();
2295
            $this->smtp->close();
2296
        }
2297
    }
2298
2299
    /**
2300
     * Set the language for error messages.
2301
     * The default language is English.
2302
     *
2303
     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
2304
     *                          Optionally, the language code can be enhanced with a 4-character
2305
     *                          script annotation and/or a 2-character country annotation.
2306
     * @param string $lang_path Path to the language file directory, with trailing separator (slash)
2307
     *                          Do not set this from user input!
2308
     *
2309
     * @return bool Returns true if the requested language was loaded, false otherwise.
2310
     */
2311
    public function setLanguage($langcode = 'en', $lang_path = '')
2312
    {
2313
        //Backwards compatibility for renamed language codes
2314
        $renamed_langcodes = [
2315
            'br' => 'pt_br',
2316
            'cz' => 'cs',
2317
            'dk' => 'da',
2318
            'no' => 'nb',
2319
            'se' => 'sv',
2320
            'rs' => 'sr',
2321
            'tg' => 'tl',
2322
            'am' => 'hy',
2323
        ];
2324
2325
        if (array_key_exists($langcode, $renamed_langcodes)) {
2326
            $langcode = $renamed_langcodes[$langcode];
2327
        }
2328
2329
        //Define full set of translatable strings in English
2330
        $PHPMAILER_LANG = [
2331
            'authenticate' => 'SMTP Error: Could not authenticate.',
2332
            'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' .
2333
                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
2334
                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
2335
            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
2336
            'data_not_accepted' => 'SMTP Error: data not accepted.',
2337
            'empty_message' => 'Message body empty',
2338
            'encoding' => 'Unknown encoding: ',
2339
            'execute' => 'Could not execute: ',
2340
            'extension_missing' => 'Extension missing: ',
2341
            'file_access' => 'Could not access file: ',
2342
            'file_open' => 'File Error: Could not open file: ',
2343
            'from_failed' => 'The following From address failed: ',
2344
            'instantiate' => 'Could not instantiate mail function.',
2345
            'invalid_address' => 'Invalid address: ',
2346
            'invalid_header' => 'Invalid header name or value',
2347
            'invalid_hostentry' => 'Invalid hostentry: ',
2348
            'invalid_host' => 'Invalid host: ',
2349
            'mailer_not_supported' => ' mailer is not supported.',
2350
            'provide_address' => 'You must provide at least one recipient email address.',
2351
            'recipients_failed' => 'SMTP Error: The following recipients failed: ',
2352
            'signing' => 'Signing Error: ',
2353
            'smtp_code' => 'SMTP code: ',
2354
            'smtp_code_ex' => 'Additional SMTP info: ',
2355
            'smtp_connect_failed' => 'SMTP connect() failed.',
2356
            'smtp_detail' => 'Detail: ',
2357
            'smtp_error' => 'SMTP server error: ',
2358
            'variable_set' => 'Cannot set or reset variable: ',
2359
        ];
2360
        if (empty($lang_path)) {
2361
            //Calculate an absolute path so it can work if CWD is not here
2362
            $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
2363
        }
2364
2365
        //Validate $langcode
2366
        $foundlang = true;
2367
        $langcode  = strtolower($langcode);
2368
        if (
2369
            !preg_match('/^(?P<lang>[a-z]{2})(?P<script>_[a-z]{4})?(?P<country>_[a-z]{2})?$/', $langcode, $matches)
2370
            && $langcode !== 'en'
2371
        ) {
2372
            $foundlang = false;
2373
            $langcode = 'en';
2374
        }
2375
2376
        //There is no English translation file
2377
        if ('en' !== $langcode) {
2378
            $langcodes = [];
2379
            if (!empty($matches['script']) && !empty($matches['country'])) {
2380
                $langcodes[] = $matches['lang'] . $matches['script'] . $matches['country'];
2381
            }
2382
            if (!empty($matches['country'])) {
2383
                $langcodes[] = $matches['lang'] . $matches['country'];
2384
            }
2385
            if (!empty($matches['script'])) {
2386
                $langcodes[] = $matches['lang'] . $matches['script'];
2387
            }
2388
            $langcodes[] = $matches['lang'];
2389
2390
            //Try and find a readable language file for the requested language.
2391
            $foundFile = false;
2392
            foreach ($langcodes as $code) {
2393
                $lang_file = $lang_path . 'phpmailer.lang-' . $code . '.php';
2394
                if (static::fileIsAccessible($lang_file)) {
2395
                    $foundFile = true;
2396
                    break;
2397
                }
2398
            }
2399
2400
            if ($foundFile === false) {
2401
                $foundlang = false;
2402
            } else {
2403
                $lines = file($lang_file);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $lang_file seems to be defined by a foreach iteration on line 2392. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
2404
                foreach ($lines as $line) {
2405
                    //Translation file lines look like this:
2406
                    //$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';
2407
                    //These files are parsed as text and not PHP so as to avoid the possibility of code injection
2408
                    //See https://blog.stevenlevithan.com/archives/match-quoted-string
2409
                    $matches = [];
2410
                    if (
2411
                        preg_match(
2412
                            '/^\$PHPMAILER_LANG\[\'([a-z\d_]+)\'\]\s*=\s*(["\'])(.+)*?\2;/',
2413
                            $line,
2414
                            $matches
2415
                        ) &&
2416
                        //Ignore unknown translation keys
2417
                        array_key_exists($matches[1], $PHPMAILER_LANG)
2418
                    ) {
2419
                        //Overwrite language-specific strings so we'll never have missing translation keys.
2420
                        $PHPMAILER_LANG[$matches[1]] = (string)$matches[3];
2421
                    }
2422
                }
2423
            }
2424
        }
2425
        $this->language = $PHPMAILER_LANG;
2426
2427
        return $foundlang; //Returns false if language not found
2428
    }
2429
2430
    /**
2431
     * Get the array of strings for the current language.
2432
     *
2433
     * @return array
2434
     */
2435
    public function getTranslations()
2436
    {
2437
        if (empty($this->language)) {
2438
            $this->setLanguage(); // Set the default language.
2439
        }
2440
2441
        return $this->language;
2442
    }
2443
2444
    /**
2445
     * Create recipient headers.
2446
     *
2447
     * @param string $type
2448
     * @param array  $addr An array of recipients,
2449
     *                     where each recipient is a 2-element indexed array with element 0 containing an address
2450
     *                     and element 1 containing a name, like:
2451
     *                     [['[email protected]', 'Joe User'], ['[email protected]', 'Zoe User']]
2452
     *
2453
     * @return string
2454
     */
2455
    public function addrAppend($type, $addr)
2456
    {
2457
        $addresses = [];
2458
        foreach ($addr as $address) {
2459
            $addresses[] = $this->addrFormat($address);
2460
        }
2461
2462
        return $type . ': ' . implode(', ', $addresses) . static::$LE;
2463
    }
2464
2465
    /**
2466
     * Format an address for use in a message header.
2467
     *
2468
     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
2469
     *                    ['[email protected]', 'Joe User']
2470
     *
2471
     * @return string
2472
     */
2473
    public function addrFormat($addr)
2474
    {
2475
        if (!isset($addr[1]) || ($addr[1] === '')) { //No name provided
2476
            return $this->secureHeader($addr[0]);
2477
        }
2478
2479
        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
2480
            ' <' . $this->secureHeader($addr[0]) . '>';
2481
    }
2482
2483
    /**
2484
     * Word-wrap message.
2485
     * For use with mailers that do not automatically perform wrapping
2486
     * and for quoted-printable encoded messages.
2487
     * Original written by philippe.
2488
     *
2489
     * @param string $message The message to wrap
2490
     * @param int    $length  The line length to wrap to
2491
     * @param bool   $qp_mode Whether to run in Quoted-Printable mode
2492
     *
2493
     * @return string
2494
     */
2495
    public function wrapText($message, $length, $qp_mode = false)
2496
    {
2497
        if ($qp_mode) {
2498
            $soft_break = sprintf(' =%s', static::$LE);
2499
        } else {
2500
            $soft_break = static::$LE;
2501
        }
2502
        //If utf-8 encoding is used, we will need to make sure we don't
2503
        //split multibyte characters when we wrap
2504
        $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
2505
        $lelen = strlen(static::$LE);
2506
        $crlflen = strlen(static::$LE);
2507
2508
        $message = static::normalizeBreaks($message);
2509
        //Remove a trailing line break
2510
        if (substr($message, -$lelen) === static::$LE) {
2511
            $message = substr($message, 0, -$lelen);
2512
        }
2513
2514
        //Split message into lines
2515
        $lines = explode(static::$LE, $message);
2516
        //Message will be rebuilt in here
2517
        $message = '';
2518
        foreach ($lines as $line) {
2519
            $words = explode(' ', $line);
2520
            $buf = '';
2521
            $firstword = true;
2522
            foreach ($words as $word) {
2523
                if ($qp_mode && (strlen($word) > $length)) {
2524
                    $space_left = $length - strlen($buf) - $crlflen;
2525
                    if (!$firstword) {
2526
                        if ($space_left > 20) {
2527
                            $len = $space_left;
2528
                            if ($is_utf8) {
2529
                                $len = $this->utf8CharBoundary($word, $len);
2530
                            } elseif ('=' === substr($word, $len - 1, 1)) {
2531
                                --$len;
2532
                            } elseif ('=' === substr($word, $len - 2, 1)) {
2533
                                $len -= 2;
2534
                            }
2535
                            $part = substr($word, 0, $len);
2536
                            $word = substr($word, $len);
2537
                            $buf .= ' ' . $part;
2538
                            $message .= $buf . sprintf('=%s', static::$LE);
2539
                        } else {
2540
                            $message .= $buf . $soft_break;
2541
                        }
2542
                        $buf = '';
2543
                    }
2544
                    while ($word !== '') {
2545
                        if ($length <= 0) {
2546
                            break;
2547
                        }
2548
                        $len = $length;
2549
                        if ($is_utf8) {
2550
                            $len = $this->utf8CharBoundary($word, $len);
2551
                        } elseif ('=' === substr($word, $len - 1, 1)) {
2552
                            --$len;
2553
                        } elseif ('=' === substr($word, $len - 2, 1)) {
2554
                            $len -= 2;
2555
                        }
2556
                        $part = substr($word, 0, $len);
2557
                        $word = (string) substr($word, $len);
2558
2559
                        if ($word !== '') {
2560
                            $message .= $part . sprintf('=%s', static::$LE);
2561
                        } else {
2562
                            $buf = $part;
2563
                        }
2564
                    }
2565
                } else {
2566
                    $buf_o = $buf;
2567
                    if (!$firstword) {
2568
                        $buf .= ' ';
2569
                    }
2570
                    $buf .= $word;
2571
2572
                    if ('' !== $buf_o && strlen($buf) > $length) {
2573
                        $message .= $buf_o . $soft_break;
2574
                        $buf = $word;
2575
                    }
2576
                }
2577
                $firstword = false;
2578
            }
2579
            $message .= $buf . static::$LE;
2580
        }
2581
2582
        return $message;
2583
    }
2584
2585
    /**
2586
     * Find the last character boundary prior to $maxLength in a utf-8
2587
     * quoted-printable encoded string.
2588
     * Original written by Colin Brown.
2589
     *
2590
     * @param string $encodedText utf-8 QP text
2591
     * @param int    $maxLength   Find the last character boundary prior to this length
2592
     *
2593
     * @return int
2594
     */
2595
    public function utf8CharBoundary($encodedText, $maxLength)
2596
    {
2597
        $foundSplitPos = false;
2598
        $lookBack = 3;
2599
        while (!$foundSplitPos) {
2600
            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
2601
            $encodedCharPos = strpos($lastChunk, '=');
2602
            if (false !== $encodedCharPos) {
2603
                //Found start of encoded character byte within $lookBack block.
2604
                //Check the encoded byte value (the 2 chars after the '=')
2605
                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
2606
                $dec = hexdec($hex);
2607
                if ($dec < 128) {
2608
                    //Single byte character.
2609
                    //If the encoded char was found at pos 0, it will fit
2610
                    //otherwise reduce maxLength to start of the encoded char
2611
                    if ($encodedCharPos > 0) {
2612
                        $maxLength -= $lookBack - $encodedCharPos;
2613
                    }
2614
                    $foundSplitPos = true;
2615
                } elseif ($dec >= 192) {
2616
                    //First byte of a multi byte character
2617
                    //Reduce maxLength to split at start of character
2618
                    $maxLength -= $lookBack - $encodedCharPos;
2619
                    $foundSplitPos = true;
2620
                } elseif ($dec < 192) {
2621
                    //Middle byte of a multi byte character, look further back
2622
                    $lookBack += 3;
2623
                }
2624
            } else {
2625
                //No encoded character found
2626
                $foundSplitPos = true;
2627
            }
2628
        }
2629
2630
        return $maxLength;
2631
    }
2632
2633
    /**
2634
     * Apply word wrapping to the message body.
2635
     * Wraps the message body to the number of chars set in the WordWrap property.
2636
     * You should only do this to plain-text bodies as wrapping HTML tags may break them.
2637
     * This is called automatically by createBody(), so you don't need to call it yourself.
2638
     */
2639
    public function setWordWrap()
2640
    {
2641
        if ($this->WordWrap < 1) {
2642
            return;
2643
        }
2644
2645
        switch ($this->message_type) {
2646
            case 'alt':
2647
            case 'alt_inline':
2648
            case 'alt_attach':
2649
            case 'alt_inline_attach':
2650
                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
2651
                break;
2652
            default:
2653
                $this->Body = $this->wrapText($this->Body, $this->WordWrap);
2654
                break;
2655
        }
2656
    }
2657
2658
    /**
2659
     * Assemble message headers.
2660
     *
2661
     * @return string The assembled headers
2662
     */
2663
    public function createHeader()
2664
    {
2665
        $result = '';
2666
2667
        $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
2668
2669
        //The To header is created automatically by mail(), so needs to be omitted here
2670
        if ('mail' !== $this->Mailer) {
2671
            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

2671
            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...
2672
                foreach ($this->to as $toaddr) {
2673
                    $this->SingleToArray[] = $this->addrFormat($toaddr);
2674
                }
2675
            } elseif (count($this->to) > 0) {
2676
                $result .= $this->addrAppend('To', $this->to);
2677
            } elseif (count($this->cc) === 0) {
2678
                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
2679
            }
2680
        }
2681
        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
2682
2683
        //sendmail and mail() extract Cc from the header before sending
2684
        if (count($this->cc) > 0) {
2685
            $result .= $this->addrAppend('Cc', $this->cc);
2686
        }
2687
2688
        //sendmail and mail() extract Bcc from the header before sending
2689
        if (
2690
            (
2691
                'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
2692
            )
2693
            && count($this->bcc) > 0
2694
        ) {
2695
            $result .= $this->addrAppend('Bcc', $this->bcc);
2696
        }
2697
2698
        if (count($this->ReplyTo) > 0) {
2699
            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
2700
        }
2701
2702
        //mail() sets the subject itself
2703
        if ('mail' !== $this->Mailer) {
2704
            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
2705
        }
2706
2707
        //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
2708
        //https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4
2709
        if (
2710
            '' !== $this->MessageID &&
2711
            preg_match(
2712
                '/^<((([a-z\d!#$%&\'*+\/=?^_`{|}~-]+(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)' .
2713
                '|("(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]|[\x21\x23-\x5B\x5D-\x7E])' .
2714
                '|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*"))@(([a-z\d!#$%&\'*+\/=?^_`{|}~-]+' .
2715
                '(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)|(\[(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]' .
2716
                '|[\x21-\x5A\x5E-\x7E])|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*\])))>$/Di',
2717
                $this->MessageID
2718
            )
2719
        ) {
2720
            $this->lastMessageID = $this->MessageID;
2721
        } else {
2722
            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
2723
        }
2724
        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
2725
        if (null !== $this->Priority) {
2726
            $result .= $this->headerLine('X-Priority', $this->Priority);
2727
        }
2728
        if ('' === $this->XMailer) {
2729
            //Empty string for default X-Mailer header
2730
            $result .= $this->headerLine(
2731
                'X-Mailer',
2732
                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
2733
            );
2734
        } elseif (is_string($this->XMailer) && trim($this->XMailer) !== '') {
2735
            //Some string
2736
            $result .= $this->headerLine('X-Mailer', trim($this->XMailer));
2737
        } //Other values result in no X-Mailer header
2738
2739
        if ('' !== $this->ConfirmReadingTo) {
2740
            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
2741
        }
2742
2743
        //Add custom headers
2744
        foreach ($this->CustomHeader as $header) {
2745
            $result .= $this->headerLine(
2746
                trim($header[0]),
2747
                $this->encodeHeader(trim($header[1]))
2748
            );
2749
        }
2750
        if (!$this->sign_key_file) {
2751
            $result .= $this->headerLine('MIME-Version', '1.0');
2752
            $result .= $this->getMailMIME();
2753
        }
2754
2755
        return $result;
2756
    }
2757
2758
    /**
2759
     * Get the message MIME type headers.
2760
     *
2761
     * @return string
2762
     */
2763
    public function getMailMIME()
2764
    {
2765
        $result = '';
2766
        $ismultipart = true;
2767
        switch ($this->message_type) {
2768
            case 'inline':
2769
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2770
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2771
                break;
2772
            case 'attach':
2773
            case 'inline_attach':
2774
            case 'alt_attach':
2775
            case 'alt_inline_attach':
2776
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';');
2777
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2778
                break;
2779
            case 'alt':
2780
            case 'alt_inline':
2781
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2782
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2783
                break;
2784
            default:
2785
                //Catches case 'plain': and case '':
2786
                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
2787
                $ismultipart = false;
2788
                break;
2789
        }
2790
        //RFC1341 part 5 says 7bit is assumed if not specified
2791
        if (static::ENCODING_7BIT !== $this->Encoding) {
2792
            //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
2793
            if ($ismultipart) {
2794
                if (static::ENCODING_8BIT === $this->Encoding) {
2795
                    $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
2796
                }
2797
                //The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
2798
            } else {
2799
                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
2800
            }
2801
        }
2802
2803
        return $result;
2804
    }
2805
2806
    /**
2807
     * Returns the whole MIME message.
2808
     * Includes complete headers and body.
2809
     * Only valid post preSend().
2810
     *
2811
     * @see PHPMailer::preSend()
2812
     *
2813
     * @return string
2814
     */
2815
    public function getSentMIMEMessage()
2816
    {
2817
        return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) .
2818
            static::$LE . static::$LE . $this->MIMEBody;
2819
    }
2820
2821
    /**
2822
     * Create a unique ID to use for boundaries.
2823
     *
2824
     * @return string
2825
     */
2826
    protected function generateId()
2827
    {
2828
        $len = 32; //32 bytes = 256 bits
2829
        $bytes = '';
2830
        if (function_exists('random_bytes')) {
2831
            try {
2832
                $bytes = random_bytes($len);
2833
            } catch (\Exception $e) {
2834
                //Do nothing
2835
            }
2836
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
2837
            /** @noinspection CryptographicallySecureRandomnessInspection */
2838
            $bytes = openssl_random_pseudo_bytes($len);
2839
        }
2840
        if ($bytes === '') {
2841
            //We failed to produce a proper random string, so make do.
2842
            //Use a hash to force the length to the same as the other methods
2843
            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
2844
        }
2845
2846
        //We don't care about messing up base64 format here, just want a random string
2847
        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
2848
    }
2849
2850
    /**
2851
     * Assemble the message body.
2852
     * Returns an empty string on failure.
2853
     *
2854
     * @throws Exception
2855
     *
2856
     * @return string The assembled message body
2857
     */
2858
    public function createBody()
2859
    {
2860
        $body = '';
2861
        //Create unique IDs and preset boundaries
2862
        $this->setBoundaries();
2863
2864
        if ($this->sign_key_file) {
2865
            $body .= $this->getMailMIME() . static::$LE;
2866
        }
2867
2868
        $this->setWordWrap();
2869
2870
        $bodyEncoding = $this->Encoding;
2871
        $bodyCharSet = $this->CharSet;
2872
        //Can we do a 7-bit downgrade?
2873
        if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {
2874
            $bodyEncoding = static::ENCODING_7BIT;
2875
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2876
            $bodyCharSet = static::CHARSET_ASCII;
2877
        }
2878
        //If lines are too long, and we're not already using an encoding that will shorten them,
2879
        //change to quoted-printable transfer encoding for the body part only
2880
        if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {
2881
            $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2882
        }
2883
2884
        $altBodyEncoding = $this->Encoding;
2885
        $altBodyCharSet = $this->CharSet;
2886
        //Can we do a 7-bit downgrade?
2887
        if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {
2888
            $altBodyEncoding = static::ENCODING_7BIT;
2889
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2890
            $altBodyCharSet = static::CHARSET_ASCII;
2891
        }
2892
        //If lines are too long, and we're not already using an encoding that will shorten them,
2893
        //change to quoted-printable transfer encoding for the alt body part only
2894
        if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {
2895
            $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2896
        }
2897
        //Use this as a preamble in all multipart message types
2898
        $mimepre = '';
2899
        switch ($this->message_type) {
2900
            case 'inline':
2901
                $body .= $mimepre;
2902
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2903
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2904
                $body .= static::$LE;
2905
                $body .= $this->attachAll('inline', $this->boundary[1]);
2906
                break;
2907
            case 'attach':
2908
                $body .= $mimepre;
2909
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2910
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2911
                $body .= static::$LE;
2912
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2913
                break;
2914
            case 'inline_attach':
2915
                $body .= $mimepre;
2916
                $body .= $this->textLine('--' . $this->boundary[1]);
2917
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2918
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
2919
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2920
                $body .= static::$LE;
2921
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
2922
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2923
                $body .= static::$LE;
2924
                $body .= $this->attachAll('inline', $this->boundary[2]);
2925
                $body .= static::$LE;
2926
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2927
                break;
2928
            case 'alt':
2929
                $body .= $mimepre;
2930
                $body .= $this->getBoundary(
2931
                    $this->boundary[1],
2932
                    $altBodyCharSet,
2933
                    static::CONTENT_TYPE_PLAINTEXT,
2934
                    $altBodyEncoding
2935
                );
2936
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2937
                $body .= static::$LE;
2938
                $body .= $this->getBoundary(
2939
                    $this->boundary[1],
2940
                    $bodyCharSet,
2941
                    static::CONTENT_TYPE_TEXT_HTML,
2942
                    $bodyEncoding
2943
                );
2944
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2945
                $body .= static::$LE;
2946
                if (!empty($this->Ical)) {
2947
                    $method = static::ICAL_METHOD_REQUEST;
2948
                    foreach (static::$IcalMethods as $imethod) {
2949
                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
2950
                            $method = $imethod;
2951
                            break;
2952
                        }
2953
                    }
2954
                    $body .= $this->getBoundary(
2955
                        $this->boundary[1],
2956
                        '',
2957
                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
2958
                        ''
2959
                    );
2960
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2961
                    $body .= static::$LE;
2962
                }
2963
                $body .= $this->endBoundary($this->boundary[1]);
2964
                break;
2965
            case 'alt_inline':
2966
                $body .= $mimepre;
2967
                $body .= $this->getBoundary(
2968
                    $this->boundary[1],
2969
                    $altBodyCharSet,
2970
                    static::CONTENT_TYPE_PLAINTEXT,
2971
                    $altBodyEncoding
2972
                );
2973
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2974
                $body .= static::$LE;
2975
                $body .= $this->textLine('--' . $this->boundary[1]);
2976
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2977
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
2978
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2979
                $body .= static::$LE;
2980
                $body .= $this->getBoundary(
2981
                    $this->boundary[2],
2982
                    $bodyCharSet,
2983
                    static::CONTENT_TYPE_TEXT_HTML,
2984
                    $bodyEncoding
2985
                );
2986
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2987
                $body .= static::$LE;
2988
                $body .= $this->attachAll('inline', $this->boundary[2]);
2989
                $body .= static::$LE;
2990
                $body .= $this->endBoundary($this->boundary[1]);
2991
                break;
2992
            case 'alt_attach':
2993
                $body .= $mimepre;
2994
                $body .= $this->textLine('--' . $this->boundary[1]);
2995
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2996
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
2997
                $body .= static::$LE;
2998
                $body .= $this->getBoundary(
2999
                    $this->boundary[2],
3000
                    $altBodyCharSet,
3001
                    static::CONTENT_TYPE_PLAINTEXT,
3002
                    $altBodyEncoding
3003
                );
3004
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
3005
                $body .= static::$LE;
3006
                $body .= $this->getBoundary(
3007
                    $this->boundary[2],
3008
                    $bodyCharSet,
3009
                    static::CONTENT_TYPE_TEXT_HTML,
3010
                    $bodyEncoding
3011
                );
3012
                $body .= $this->encodeString($this->Body, $bodyEncoding);
3013
                $body .= static::$LE;
3014
                if (!empty($this->Ical)) {
3015
                    $method = static::ICAL_METHOD_REQUEST;
3016
                    foreach (static::$IcalMethods as $imethod) {
3017
                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
3018
                            $method = $imethod;
3019
                            break;
3020
                        }
3021
                    }
3022
                    $body .= $this->getBoundary(
3023
                        $this->boundary[2],
3024
                        '',
3025
                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
3026
                        ''
3027
                    );
3028
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
3029
                }
3030
                $body .= $this->endBoundary($this->boundary[2]);
3031
                $body .= static::$LE;
3032
                $body .= $this->attachAll('attachment', $this->boundary[1]);
3033
                break;
3034
            case 'alt_inline_attach':
3035
                $body .= $mimepre;
3036
                $body .= $this->textLine('--' . $this->boundary[1]);
3037
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
3038
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
3039
                $body .= static::$LE;
3040
                $body .= $this->getBoundary(
3041
                    $this->boundary[2],
3042
                    $altBodyCharSet,
3043
                    static::CONTENT_TYPE_PLAINTEXT,
3044
                    $altBodyEncoding
3045
                );
3046
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
3047
                $body .= static::$LE;
3048
                $body .= $this->textLine('--' . $this->boundary[2]);
3049
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
3050
                $body .= $this->textLine(' boundary="' . $this->boundary[3] . '";');
3051
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
3052
                $body .= static::$LE;
3053
                $body .= $this->getBoundary(
3054
                    $this->boundary[3],
3055
                    $bodyCharSet,
3056
                    static::CONTENT_TYPE_TEXT_HTML,
3057
                    $bodyEncoding
3058
                );
3059
                $body .= $this->encodeString($this->Body, $bodyEncoding);
3060
                $body .= static::$LE;
3061
                $body .= $this->attachAll('inline', $this->boundary[3]);
3062
                $body .= static::$LE;
3063
                $body .= $this->endBoundary($this->boundary[2]);
3064
                $body .= static::$LE;
3065
                $body .= $this->attachAll('attachment', $this->boundary[1]);
3066
                break;
3067
            default:
3068
                //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
3069
                //Reset the `Encoding` property in case we changed it for line length reasons
3070
                $this->Encoding = $bodyEncoding;
3071
                $body .= $this->encodeString($this->Body, $this->Encoding);
3072
                break;
3073
        }
3074
3075
        if ($this->isError()) {
3076
            $body = '';
3077
            if ($this->exceptions) {
3078
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
3079
            }
3080
        } elseif ($this->sign_key_file) {
3081
            try {
3082
                if (!defined('PKCS7_TEXT')) {
3083
                    throw new Exception($this->lang('extension_missing') . 'openssl');
3084
                }
3085
3086
                $file = tempnam(sys_get_temp_dir(), 'srcsign');
3087
                $signed = tempnam(sys_get_temp_dir(), 'mailsign');
3088
                file_put_contents($file, $body);
3089
3090
                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
3091
                if (empty($this->sign_extracerts_file)) {
3092
                    $sign = @openssl_pkcs7_sign(
3093
                        $file,
3094
                        $signed,
3095
                        'file://' . realpath($this->sign_cert_file),
3096
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
3097
                        []
3098
                    );
3099
                } else {
3100
                    $sign = @openssl_pkcs7_sign(
3101
                        $file,
3102
                        $signed,
3103
                        'file://' . realpath($this->sign_cert_file),
3104
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
3105
                        [],
3106
                        PKCS7_DETACHED,
3107
                        $this->sign_extracerts_file
3108
                    );
3109
                }
3110
3111
                @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

3111
                /** @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...
3112
                if ($sign) {
3113
                    $body = file_get_contents($signed);
3114
                    @unlink($signed);
3115
                    //The message returned by openssl contains both headers and body, so need to split them up
3116
                    $parts = explode("\n\n", $body, 2);
3117
                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
3118
                    $body = $parts[1];
3119
                } else {
3120
                    @unlink($signed);
3121
                    throw new Exception($this->lang('signing') . openssl_error_string());
3122
                }
3123
            } catch (Exception $exc) {
3124
                $body = '';
3125
                if ($this->exceptions) {
3126
                    throw $exc;
3127
                }
3128
            }
3129
        }
3130
3131
        return $body;
3132
    }
3133
3134
    /**
3135
     * Get the boundaries that this message will use
3136
     * @return array
3137
     */
3138
    public function getBoundaries()
3139
    {
3140
        if (empty($this->boundary)) {
3141
            $this->setBoundaries();
3142
        }
3143
        return $this->boundary;
3144
    }
3145
3146
    /**
3147
     * Return the start of a message boundary.
3148
     *
3149
     * @param string $boundary
3150
     * @param string $charSet
3151
     * @param string $contentType
3152
     * @param string $encoding
3153
     *
3154
     * @return string
3155
     */
3156
    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
3157
    {
3158
        $result = '';
3159
        if ('' === $charSet) {
3160
            $charSet = $this->CharSet;
3161
        }
3162
        if ('' === $contentType) {
3163
            $contentType = $this->ContentType;
3164
        }
3165
        if ('' === $encoding) {
3166
            $encoding = $this->Encoding;
3167
        }
3168
        $result .= $this->textLine('--' . $boundary);
3169
        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
3170
        $result .= static::$LE;
3171
        //RFC1341 part 5 says 7bit is assumed if not specified
3172
        if (static::ENCODING_7BIT !== $encoding) {
3173
            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
3174
        }
3175
        $result .= static::$LE;
3176
3177
        return $result;
3178
    }
3179
3180
    /**
3181
     * Return the end of a message boundary.
3182
     *
3183
     * @param string $boundary
3184
     *
3185
     * @return string
3186
     */
3187
    protected function endBoundary($boundary)
3188
    {
3189
        return static::$LE . '--' . $boundary . '--' . static::$LE;
3190
    }
3191
3192
    /**
3193
     * Set the message type.
3194
     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
3195
     */
3196
    protected function setMessageType()
3197
    {
3198
        $type = [];
3199
        if ($this->alternativeExists()) {
3200
            $type[] = 'alt';
3201
        }
3202
        if ($this->inlineImageExists()) {
3203
            $type[] = 'inline';
3204
        }
3205
        if ($this->attachmentExists()) {
3206
            $type[] = 'attach';
3207
        }
3208
        $this->message_type = implode('_', $type);
3209
        if ('' === $this->message_type) {
3210
            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
3211
            $this->message_type = 'plain';
3212
        }
3213
    }
3214
3215
    /**
3216
     * Format a header line.
3217
     *
3218
     * @param string     $name
3219
     * @param string|int $value
3220
     *
3221
     * @return string
3222
     */
3223
    public function headerLine($name, $value)
3224
    {
3225
        return $name . ': ' . $value . static::$LE;
3226
    }
3227
3228
    /**
3229
     * Return a formatted mail line.
3230
     *
3231
     * @param string $value
3232
     *
3233
     * @return string
3234
     */
3235
    public function textLine($value)
3236
    {
3237
        return $value . static::$LE;
3238
    }
3239
3240
    /**
3241
     * Add an attachment from a path on the filesystem.
3242
     * Never use a user-supplied path to a file!
3243
     * Returns false if the file could not be found or read.
3244
     * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.
3245
     * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
3246
     *
3247
     * @param string $path        Path to the attachment
3248
     * @param string $name        Overrides the attachment name
3249
     * @param string $encoding    File encoding (see $Encoding)
3250
     * @param string $type        MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
3251
     * @param string $disposition Disposition to use
3252
     *
3253
     * @throws Exception
3254
     *
3255
     * @return bool
3256
     */
3257
    public function addAttachment(
3258
        $path,
3259
        $name = '',
3260
        $encoding = self::ENCODING_BASE64,
3261
        $type = '',
3262
        $disposition = 'attachment'
3263
    ) {
3264
        try {
3265
            if (!static::fileIsAccessible($path)) {
3266
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
3267
            }
3268
3269
            //If a MIME type is not specified, try to work it out from the file name
3270
            if ('' === $type) {
3271
                $type = static::filenameToType($path);
3272
            }
3273
3274
            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
3275
            if ('' === $name) {
3276
                $name = $filename;
3277
            }
3278
            if (!$this->validateEncoding($encoding)) {
3279
                throw new Exception($this->lang('encoding') . $encoding);
3280
            }
3281
3282
            $this->attachment[] = [
3283
                0 => $path,
3284
                1 => $filename,
3285
                2 => $name,
3286
                3 => $encoding,
3287
                4 => $type,
3288
                5 => false, //isStringAttachment
3289
                6 => $disposition,
3290
                7 => $name,
3291
            ];
3292
        } catch (Exception $exc) {
3293
            $this->setError($exc->getMessage());
3294
            $this->edebug($exc->getMessage());
3295
            if ($this->exceptions) {
3296
                throw $exc;
3297
            }
3298
3299
            return false;
3300
        }
3301
3302
        return true;
3303
    }
3304
3305
    /**
3306
     * Return the array of attachments.
3307
     *
3308
     * @return array
3309
     */
3310
    public function getAttachments()
3311
    {
3312
        return $this->attachment;
3313
    }
3314
3315
    /**
3316
     * Attach all file, string, and binary attachments to the message.
3317
     * Returns an empty string on failure.
3318
     *
3319
     * @param string $disposition_type
3320
     * @param string $boundary
3321
     *
3322
     * @throws Exception
3323
     *
3324
     * @return string
3325
     */
3326
    protected function attachAll($disposition_type, $boundary)
3327
    {
3328
        //Return text of body
3329
        $mime = [];
3330
        $cidUniq = [];
3331
        $incl = [];
3332
3333
        //Add all attachments
3334
        foreach ($this->attachment as $attachment) {
3335
            //Check if it is a valid disposition_filter
3336
            if ($attachment[6] === $disposition_type) {
3337
                //Check for string attachment
3338
                $string = '';
3339
                $path = '';
3340
                $bString = $attachment[5];
3341
                if ($bString) {
3342
                    $string = $attachment[0];
3343
                } else {
3344
                    $path = $attachment[0];
3345
                }
3346
3347
                $inclhash = hash('sha256', serialize($attachment));
3348
                if (in_array($inclhash, $incl, true)) {
3349
                    continue;
3350
                }
3351
                $incl[] = $inclhash;
3352
                $name = $attachment[2];
3353
                $encoding = $attachment[3];
3354
                $type = $attachment[4];
3355
                $disposition = $attachment[6];
3356
                $cid = $attachment[7];
3357
                if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {
3358
                    continue;
3359
                }
3360
                $cidUniq[$cid] = true;
3361
3362
                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
3363
                //Only include a filename property if we have one
3364
                if (!empty($name)) {
3365
                    $mime[] = sprintf(
3366
                        'Content-Type: %s; name=%s%s',
3367
                        $type,
3368
                        static::quotedString($this->encodeHeader($this->secureHeader($name))),
3369
                        static::$LE
3370
                    );
3371
                } else {
3372
                    $mime[] = sprintf(
3373
                        'Content-Type: %s%s',
3374
                        $type,
3375
                        static::$LE
3376
                    );
3377
                }
3378
                //RFC1341 part 5 says 7bit is assumed if not specified
3379
                if (static::ENCODING_7BIT !== $encoding) {
3380
                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
3381
                }
3382
3383
                //Only set Content-IDs on inline attachments
3384
                if ((string) $cid !== '' && $disposition === 'inline') {
3385
                    $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
3386
                }
3387
3388
                //Allow for bypassing the Content-Disposition header
3389
                if (!empty($disposition)) {
3390
                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
3391
                    if (!empty($encoded_name)) {
3392
                        $mime[] = sprintf(
3393
                            'Content-Disposition: %s; filename=%s%s',
3394
                            $disposition,
3395
                            static::quotedString($encoded_name),
3396
                            static::$LE . static::$LE
3397
                        );
3398
                    } else {
3399
                        $mime[] = sprintf(
3400
                            'Content-Disposition: %s%s',
3401
                            $disposition,
3402
                            static::$LE . static::$LE
3403
                        );
3404
                    }
3405
                } else {
3406
                    $mime[] = static::$LE;
3407
                }
3408
3409
                //Encode as string attachment
3410
                if ($bString) {
3411
                    $mime[] = $this->encodeString($string, $encoding);
3412
                } else {
3413
                    $mime[] = $this->encodeFile($path, $encoding);
3414
                }
3415
                if ($this->isError()) {
3416
                    return '';
3417
                }
3418
                $mime[] = static::$LE;
3419
            }
3420
        }
3421
3422
        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
3423
3424
        return implode('', $mime);
3425
    }
3426
3427
    /**
3428
     * Encode a file attachment in requested format.
3429
     * Returns an empty string on failure.
3430
     *
3431
     * @param string $path     The full path to the file
3432
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3433
     *
3434
     * @return string
3435
     */
3436
    protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
3437
    {
3438
        try {
3439
            if (!static::fileIsAccessible($path)) {
3440
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
3441
            }
3442
            $file_buffer = file_get_contents($path);
3443
            if (false === $file_buffer) {
3444
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
3445
            }
3446
            $file_buffer = $this->encodeString($file_buffer, $encoding);
3447
3448
            return $file_buffer;
3449
        } catch (Exception $exc) {
3450
            $this->setError($exc->getMessage());
3451
            $this->edebug($exc->getMessage());
3452
            if ($this->exceptions) {
3453
                throw $exc;
3454
            }
3455
3456
            return '';
3457
        }
3458
    }
3459
3460
    /**
3461
     * Encode a string in requested format.
3462
     * Returns an empty string on failure.
3463
     *
3464
     * @param string $str      The text to encode
3465
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3466
     *
3467
     * @throws Exception
3468
     *
3469
     * @return string
3470
     */
3471
    public function encodeString($str, $encoding = self::ENCODING_BASE64)
3472
    {
3473
        $encoded = '';
3474
        switch (strtolower($encoding)) {
3475
            case static::ENCODING_BASE64:
3476
                $encoded = chunk_split(
3477
                    base64_encode($str),
3478
                    static::STD_LINE_LENGTH,
3479
                    static::$LE
3480
                );
3481
                break;
3482
            case static::ENCODING_7BIT:
3483
            case static::ENCODING_8BIT:
3484
                $encoded = static::normalizeBreaks($str);
3485
                //Make sure it ends with a line break
3486
                if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
3487
                    $encoded .= static::$LE;
3488
                }
3489
                break;
3490
            case static::ENCODING_BINARY:
3491
                $encoded = $str;
3492
                break;
3493
            case static::ENCODING_QUOTED_PRINTABLE:
3494
                $encoded = $this->encodeQP($str);
3495
                break;
3496
            default:
3497
                $this->setError($this->lang('encoding') . $encoding);
3498
                if ($this->exceptions) {
3499
                    throw new Exception($this->lang('encoding') . $encoding);
3500
                }
3501
                break;
3502
        }
3503
3504
        return $encoded;
3505
    }
3506
3507
    /**
3508
     * Encode a header value (not including its label) optimally.
3509
     * Picks shortest of Q, B, or none. Result includes folding if needed.
3510
     * See RFC822 definitions for phrase, comment and text positions.
3511
     *
3512
     * @param string $str      The header value to encode
3513
     * @param string $position What context the string will be used in
3514
     *
3515
     * @return string
3516
     */
3517
    public function encodeHeader($str, $position = 'text')
3518
    {
3519
        $matchcount = 0;
3520
        switch (strtolower($position)) {
3521
            case 'phrase':
3522
                if (!preg_match('/[\200-\377]/', $str)) {
3523
                    //Can't use addslashes as we don't know the value of magic_quotes_sybase
3524
                    $encoded = addcslashes($str, "\0..\37\177\\\"");
3525
                    if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
3526
                        return $encoded;
3527
                    }
3528
3529
                    return "\"$encoded\"";
3530
                }
3531
                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
3532
                break;
3533
            /* @noinspection PhpMissingBreakStatementInspection */
3534
            case 'comment':
3535
                $matchcount = preg_match_all('/[()"]/', $str, $matches);
3536
            //fallthrough
3537
            case 'text':
3538
            default:
3539
                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
3540
                break;
3541
        }
3542
3543
        if ($this->has8bitChars($str)) {
3544
            $charset = $this->CharSet;
3545
        } else {
3546
            $charset = static::CHARSET_ASCII;
3547
        }
3548
3549
        //Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
3550
        $overhead = 8 + strlen($charset);
3551
3552
        if ('mail' === $this->Mailer) {
3553
            $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
3554
        } else {
3555
            $maxlen = static::MAX_LINE_LENGTH - $overhead;
3556
        }
3557
3558
        //Select the encoding that produces the shortest output and/or prevents corruption.
3559
        if ($matchcount > strlen($str) / 3) {
3560
            //More than 1/3 of the content needs encoding, use B-encode.
3561
            $encoding = 'B';
3562
        } elseif ($matchcount > 0) {
3563
            //Less than 1/3 of the content needs encoding, use Q-encode.
3564
            $encoding = 'Q';
3565
        } elseif (strlen($str) > $maxlen) {
3566
            //No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
3567
            $encoding = 'Q';
3568
        } else {
3569
            //No reformatting needed
3570
            $encoding = false;
3571
        }
3572
3573
        switch ($encoding) {
3574
            case 'B':
3575
                if ($this->hasMultiBytes($str)) {
3576
                    //Use a custom function which correctly encodes and wraps long
3577
                    //multibyte strings without breaking lines within a character
3578
                    $encoded = $this->base64EncodeWrapMB($str, "\n");
3579
                } else {
3580
                    $encoded = base64_encode($str);
3581
                    $maxlen -= $maxlen % 4;
3582
                    $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
3583
                }
3584
                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
3585
                break;
3586
            case 'Q':
3587
                $encoded = $this->encodeQ($str, $position);
3588
                $encoded = $this->wrapText($encoded, $maxlen, true);
3589
                $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
3590
                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
3591
                break;
3592
            default:
3593
                return $str;
3594
        }
3595
3596
        return trim(static::normalizeBreaks($encoded));
3597
    }
3598
3599
    /**
3600
     * Check if a string contains multi-byte characters.
3601
     *
3602
     * @param string $str multi-byte text to wrap encode
3603
     *
3604
     * @return bool
3605
     */
3606
    public function hasMultiBytes($str)
3607
    {
3608
        if (function_exists('mb_strlen')) {
3609
            return strlen($str) > mb_strlen($str, $this->CharSet);
3610
        }
3611
3612
        //Assume no multibytes (we can't handle without mbstring functions anyway)
3613
        return false;
3614
    }
3615
3616
    /**
3617
     * Does a string contain any 8-bit chars (in any charset)?
3618
     *
3619
     * @param string $text
3620
     *
3621
     * @return bool
3622
     */
3623
    public function has8bitChars($text)
3624
    {
3625
        return (bool) preg_match('/[\x80-\xFF]/', $text);
3626
    }
3627
3628
    /**
3629
     * Encode and wrap long multibyte strings for mail headers
3630
     * without breaking lines within a character.
3631
     * Adapted from a function by paravoid.
3632
     *
3633
     * @see https://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
3634
     *
3635
     * @param string $str       multi-byte text to wrap encode
3636
     * @param string $linebreak string to use as linefeed/end-of-line
3637
     *
3638
     * @return string
3639
     */
3640
    public function base64EncodeWrapMB($str, $linebreak = null)
3641
    {
3642
        $start = '=?' . $this->CharSet . '?B?';
3643
        $end = '?=';
3644
        $encoded = '';
3645
        if (null === $linebreak) {
3646
            $linebreak = static::$LE;
3647
        }
3648
3649
        $mb_length = mb_strlen($str, $this->CharSet);
3650
        //Each line must have length <= 75, including $start and $end
3651
        $length = 75 - strlen($start) - strlen($end);
3652
        //Average multi-byte ratio
3653
        $ratio = $mb_length / strlen($str);
3654
        //Base64 has a 4:3 ratio
3655
        $avgLength = floor($length * $ratio * .75);
3656
3657
        $offset = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $offset is dead and can be removed.
Loading history...
3658
        for ($i = 0; $i < $mb_length; $i += $offset) {
3659
            $lookBack = 0;
3660
            do {
3661
                $offset = $avgLength - $lookBack;
3662
                $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

3662
                $chunk = mb_substr($str, $i, /** @scrutinizer ignore-type */ $offset, $this->CharSet);
Loading history...
3663
                $chunk = base64_encode($chunk);
3664
                ++$lookBack;
3665
            } while (strlen($chunk) > $length);
3666
            $encoded .= $chunk . $linebreak;
3667
        }
3668
3669
        //Chomp the last linefeed
3670
        return substr($encoded, 0, -strlen($linebreak));
3671
    }
3672
3673
    /**
3674
     * Encode a string in quoted-printable format.
3675
     * According to RFC2045 section 6.7.
3676
     *
3677
     * @param string $string The text to encode
3678
     *
3679
     * @return string
3680
     */
3681
    public function encodeQP($string)
3682
    {
3683
        return static::normalizeBreaks(quoted_printable_encode($string));
3684
    }
3685
3686
    /**
3687
     * Encode a string using Q encoding.
3688
     *
3689
     * @see https://www.rfc-editor.org/rfc/rfc2047#section-4.2
3690
     *
3691
     * @param string $str      the text to encode
3692
     * @param string $position Where the text is going to be used, see the RFC for what that means
3693
     *
3694
     * @return string
3695
     */
3696
    public function encodeQ($str, $position = 'text')
3697
    {
3698
        //There should not be any EOL in the string
3699
        $pattern = '';
3700
        $encoded = str_replace(["\r", "\n"], '', $str);
3701
        switch (strtolower($position)) {
3702
            case 'phrase':
3703
                //RFC 2047 section 5.3
3704
                $pattern = '^A-Za-z0-9!*+\/ -';
3705
                break;
3706
            /*
3707
             * RFC 2047 section 5.2.
3708
             * Build $pattern without including delimiters and []
3709
             */
3710
            /* @noinspection PhpMissingBreakStatementInspection */
3711
            case 'comment':
3712
                $pattern = '\(\)"';
3713
            /* Intentional fall through */
3714
            case 'text':
3715
            default:
3716
                //RFC 2047 section 5.1
3717
                //Replace every high ascii, control, =, ? and _ characters
3718
                $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
3719
                break;
3720
        }
3721
        $matches = [];
3722
        if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
3723
            //If the string contains an '=', make sure it's the first thing we replace
3724
            //so as to avoid double-encoding
3725
            $eqkey = array_search('=', $matches[0], true);
3726
            if (false !== $eqkey) {
3727
                unset($matches[0][$eqkey]);
3728
                array_unshift($matches[0], '=');
3729
            }
3730
            foreach (array_unique($matches[0]) as $char) {
3731
                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
3732
            }
3733
        }
3734
        //Replace spaces with _ (more readable than =20)
3735
        //RFC 2047 section 4.2(2)
3736
        return str_replace(' ', '_', $encoded);
3737
    }
3738
3739
    /**
3740
     * Add a string or binary attachment (non-filesystem).
3741
     * This method can be used to attach ascii or binary data,
3742
     * such as a BLOB record from a database.
3743
     *
3744
     * @param string $string      String attachment data
3745
     * @param string $filename    Name of the attachment
3746
     * @param string $encoding    File encoding (see $Encoding)
3747
     * @param string $type        File extension (MIME) type
3748
     * @param string $disposition Disposition to use
3749
     *
3750
     * @throws Exception
3751
     *
3752
     * @return bool True on successfully adding an attachment
3753
     */
3754
    public function addStringAttachment(
3755
        $string,
3756
        $filename,
3757
        $encoding = self::ENCODING_BASE64,
3758
        $type = '',
3759
        $disposition = 'attachment'
3760
    ) {
3761
        try {
3762
            //If a MIME type is not specified, try to work it out from the file name
3763
            if ('' === $type) {
3764
                $type = static::filenameToType($filename);
3765
            }
3766
3767
            if (!$this->validateEncoding($encoding)) {
3768
                throw new Exception($this->lang('encoding') . $encoding);
3769
            }
3770
3771
            //Append to $attachment array
3772
            $this->attachment[] = [
3773
                0 => $string,
3774
                1 => $filename,
3775
                2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
3776
                3 => $encoding,
3777
                4 => $type,
3778
                5 => true, //isStringAttachment
3779
                6 => $disposition,
3780
                7 => 0,
3781
            ];
3782
        } catch (Exception $exc) {
3783
            $this->setError($exc->getMessage());
3784
            $this->edebug($exc->getMessage());
3785
            if ($this->exceptions) {
3786
                throw $exc;
3787
            }
3788
3789
            return false;
3790
        }
3791
3792
        return true;
3793
    }
3794
3795
    /**
3796
     * Add an embedded (inline) attachment from a file.
3797
     * This can include images, sounds, and just about any other document type.
3798
     * These differ from 'regular' attachments in that they are intended to be
3799
     * displayed inline with the message, not just attached for download.
3800
     * This is used in HTML messages that embed the images
3801
     * the HTML refers to using the `$cid` value in `img` tags, for example `<img src="cid:mylogo">`.
3802
     * Never use a user-supplied path to a file!
3803
     *
3804
     * @param string $path        Path to the attachment
3805
     * @param string $cid         Content ID of the attachment; Use this to reference
3806
     *                            the content when using an embedded image in HTML
3807
     * @param string $name        Overrides the attachment filename
3808
     * @param string $encoding    File encoding (see $Encoding) defaults to `base64`
3809
     * @param string $type        File MIME type (by default mapped from the `$path` filename's extension)
3810
     * @param string $disposition Disposition to use: `inline` (default) or `attachment`
3811
     *                            (unlikely you want this – {@see `addAttachment()`} instead)
3812
     *
3813
     * @return bool True on successfully adding an attachment
3814
     * @throws Exception
3815
     *
3816
     */
3817
    public function addEmbeddedImage(
3818
        $path,
3819
        $cid,
3820
        $name = '',
3821
        $encoding = self::ENCODING_BASE64,
3822
        $type = '',
3823
        $disposition = 'inline'
3824
    ) {
3825
        try {
3826
            if (!static::fileIsAccessible($path)) {
3827
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
3828
            }
3829
3830
            //If a MIME type is not specified, try to work it out from the file name
3831
            if ('' === $type) {
3832
                $type = static::filenameToType($path);
3833
            }
3834
3835
            if (!$this->validateEncoding($encoding)) {
3836
                throw new Exception($this->lang('encoding') . $encoding);
3837
            }
3838
3839
            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
3840
            if ('' === $name) {
3841
                $name = $filename;
3842
            }
3843
3844
            //Append to $attachment array
3845
            $this->attachment[] = [
3846
                0 => $path,
3847
                1 => $filename,
3848
                2 => $name,
3849
                3 => $encoding,
3850
                4 => $type,
3851
                5 => false, //isStringAttachment
3852
                6 => $disposition,
3853
                7 => $cid,
3854
            ];
3855
        } catch (Exception $exc) {
3856
            $this->setError($exc->getMessage());
3857
            $this->edebug($exc->getMessage());
3858
            if ($this->exceptions) {
3859
                throw $exc;
3860
            }
3861
3862
            return false;
3863
        }
3864
3865
        return true;
3866
    }
3867
3868
    /**
3869
     * Add an embedded stringified attachment.
3870
     * This can include images, sounds, and just about any other document type.
3871
     * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.
3872
     *
3873
     * @param string $string      The attachment binary data
3874
     * @param string $cid         Content ID of the attachment; Use this to reference
3875
     *                            the content when using an embedded image in HTML
3876
     * @param string $name        A filename for the attachment. If this contains an extension,
3877
     *                            PHPMailer will attempt to set a MIME type for the attachment.
3878
     *                            For example 'file.jpg' would get an 'image/jpeg' MIME type.
3879
     * @param string $encoding    File encoding (see $Encoding), defaults to 'base64'
3880
     * @param string $type        MIME type - will be used in preference to any automatically derived type
3881
     * @param string $disposition Disposition to use
3882
     *
3883
     * @throws Exception
3884
     *
3885
     * @return bool True on successfully adding an attachment
3886
     */
3887
    public function addStringEmbeddedImage(
3888
        $string,
3889
        $cid,
3890
        $name = '',
3891
        $encoding = self::ENCODING_BASE64,
3892
        $type = '',
3893
        $disposition = 'inline'
3894
    ) {
3895
        try {
3896
            //If a MIME type is not specified, try to work it out from the name
3897
            if ('' === $type && !empty($name)) {
3898
                $type = static::filenameToType($name);
3899
            }
3900
3901
            if (!$this->validateEncoding($encoding)) {
3902
                throw new Exception($this->lang('encoding') . $encoding);
3903
            }
3904
3905
            //Append to $attachment array
3906
            $this->attachment[] = [
3907
                0 => $string,
3908
                1 => $name,
3909
                2 => $name,
3910
                3 => $encoding,
3911
                4 => $type,
3912
                5 => true, //isStringAttachment
3913
                6 => $disposition,
3914
                7 => $cid,
3915
            ];
3916
        } catch (Exception $exc) {
3917
            $this->setError($exc->getMessage());
3918
            $this->edebug($exc->getMessage());
3919
            if ($this->exceptions) {
3920
                throw $exc;
3921
            }
3922
3923
            return false;
3924
        }
3925
3926
        return true;
3927
    }
3928
3929
    /**
3930
     * Validate encodings.
3931
     *
3932
     * @param string $encoding
3933
     *
3934
     * @return bool
3935
     */
3936
    protected function validateEncoding($encoding)
3937
    {
3938
        return in_array(
3939
            $encoding,
3940
            [
3941
                self::ENCODING_7BIT,
3942
                self::ENCODING_QUOTED_PRINTABLE,
3943
                self::ENCODING_BASE64,
3944
                self::ENCODING_8BIT,
3945
                self::ENCODING_BINARY,
3946
            ],
3947
            true
3948
        );
3949
    }
3950
3951
    /**
3952
     * Check if an embedded attachment is present with this cid.
3953
     *
3954
     * @param string $cid
3955
     *
3956
     * @return bool
3957
     */
3958
    protected function cidExists($cid)
3959
    {
3960
        foreach ($this->attachment as $attachment) {
3961
            if ('inline' === $attachment[6] && $cid === $attachment[7]) {
3962
                return true;
3963
            }
3964
        }
3965
3966
        return false;
3967
    }
3968
3969
    /**
3970
     * Check if an inline attachment is present.
3971
     *
3972
     * @return bool
3973
     */
3974
    public function inlineImageExists()
3975
    {
3976
        foreach ($this->attachment as $attachment) {
3977
            if ('inline' === $attachment[6]) {
3978
                return true;
3979
            }
3980
        }
3981
3982
        return false;
3983
    }
3984
3985
    /**
3986
     * Check if an attachment (non-inline) is present.
3987
     *
3988
     * @return bool
3989
     */
3990
    public function attachmentExists()
3991
    {
3992
        foreach ($this->attachment as $attachment) {
3993
            if ('attachment' === $attachment[6]) {
3994
                return true;
3995
            }
3996
        }
3997
3998
        return false;
3999
    }
4000
4001
    /**
4002
     * Check if this message has an alternative body set.
4003
     *
4004
     * @return bool
4005
     */
4006
    public function alternativeExists()
4007
    {
4008
        return !empty($this->AltBody);
4009
    }
4010
4011
    /**
4012
     * Clear queued addresses of given kind.
4013
     *
4014
     * @param string $kind 'to', 'cc', or 'bcc'
4015
     */
4016
    public function clearQueuedAddresses($kind)
4017
    {
4018
        $this->RecipientsQueue = array_filter(
4019
            $this->RecipientsQueue,
4020
            static function ($params) use ($kind) {
4021
                return $params[0] !== $kind;
4022
            }
4023
        );
4024
    }
4025
4026
    /**
4027
     * Clear all To recipients.
4028
     */
4029
    public function clearAddresses()
4030
    {
4031
        foreach ($this->to as $to) {
4032
            unset($this->all_recipients[strtolower($to[0])]);
4033
        }
4034
        $this->to = [];
4035
        $this->clearQueuedAddresses('to');
4036
    }
4037
4038
    /**
4039
     * Clear all CC recipients.
4040
     */
4041
    public function clearCCs()
4042
    {
4043
        foreach ($this->cc as $cc) {
4044
            unset($this->all_recipients[strtolower($cc[0])]);
4045
        }
4046
        $this->cc = [];
4047
        $this->clearQueuedAddresses('cc');
4048
    }
4049
4050
    /**
4051
     * Clear all BCC recipients.
4052
     */
4053
    public function clearBCCs()
4054
    {
4055
        foreach ($this->bcc as $bcc) {
4056
            unset($this->all_recipients[strtolower($bcc[0])]);
4057
        }
4058
        $this->bcc = [];
4059
        $this->clearQueuedAddresses('bcc');
4060
    }
4061
4062
    /**
4063
     * Clear all ReplyTo recipients.
4064
     */
4065
    public function clearReplyTos()
4066
    {
4067
        $this->ReplyTo = [];
4068
        $this->ReplyToQueue = [];
4069
    }
4070
4071
    /**
4072
     * Clear all recipient types.
4073
     */
4074
    public function clearAllRecipients()
4075
    {
4076
        $this->to = [];
4077
        $this->cc = [];
4078
        $this->bcc = [];
4079
        $this->all_recipients = [];
4080
        $this->RecipientsQueue = [];
4081
    }
4082
4083
    /**
4084
     * Clear all filesystem, string, and binary attachments.
4085
     */
4086
    public function clearAttachments()
4087
    {
4088
        $this->attachment = [];
4089
    }
4090
4091
    /**
4092
     * Clear all custom headers.
4093
     */
4094
    public function clearCustomHeaders()
4095
    {
4096
        $this->CustomHeader = [];
4097
    }
4098
4099
    /**
4100
     * Clear a specific custom header by name or name and value.
4101
     * $name value can be overloaded to contain
4102
     * both header name and value (name:value).
4103
     *
4104
     * @param string      $name  Custom header name
4105
     * @param string|null $value Header value
4106
     *
4107
     * @return bool True if a header was replaced successfully
4108
     */
4109
    public function clearCustomHeader($name, $value = null)
4110
    {
4111
        if (null === $value && strpos($name, ':') !== false) {
4112
            //Value passed in as name:value
4113
            list($name, $value) = explode(':', $name, 2);
4114
        }
4115
        $name = trim($name);
4116
        $value = (null === $value) ? null : trim($value);
4117
4118
        foreach ($this->CustomHeader as $k => $pair) {
4119
            if ($pair[0] == $name) {
4120
                // We remove the header if the value is not provided or it matches.
4121
                if (null === $value ||  $pair[1] == $value) {
4122
                    unset($this->CustomHeader[$k]);
4123
                }
4124
            }
4125
        }
4126
4127
        return true;
4128
    }
4129
4130
    /**
4131
     * Replace a custom header.
4132
     * $name value can be overloaded to contain
4133
     * both header name and value (name:value).
4134
     *
4135
     * @param string      $name  Custom header name
4136
     * @param string|null $value Header value
4137
     *
4138
     * @return bool True if a header was replaced successfully
4139
     * @throws Exception
4140
     */
4141
    public function replaceCustomHeader($name, $value = null)
4142
    {
4143
        if (null === $value && strpos($name, ':') !== false) {
4144
            //Value passed in as name:value
4145
            list($name, $value) = explode(':', $name, 2);
4146
        }
4147
        $name = trim($name);
4148
        $value = (null === $value) ? '' : trim($value);
4149
4150
        $replaced = false;
4151
        foreach ($this->CustomHeader as $k => $pair) {
4152
            if ($pair[0] == $name) {
4153
                if ($replaced) {
4154
                    unset($this->CustomHeader[$k]);
4155
                    continue;
4156
                }
4157
                if (strpbrk($name . $value, "\r\n") !== false) {
4158
                    if ($this->exceptions) {
4159
                        throw new Exception($this->lang('invalid_header'));
4160
                    }
4161
4162
                    return false;
4163
                }
4164
                $this->CustomHeader[$k] = [$name, $value];
4165
                $replaced = true;
4166
            }
4167
        }
4168
4169
        return true;
4170
    }
4171
4172
    /**
4173
     * Add an error message to the error container.
4174
     *
4175
     * @param string $msg
4176
     */
4177
    protected function setError($msg)
4178
    {
4179
        ++$this->error_count;
4180
        if ('smtp' === $this->Mailer && null !== $this->smtp) {
4181
            $lasterror = $this->smtp->getError();
4182
            if (!empty($lasterror['error'])) {
4183
                $msg .= $this->lang('smtp_error') . $lasterror['error'];
4184
                if (!empty($lasterror['detail'])) {
4185
                    $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail'];
4186
                }
4187
                if (!empty($lasterror['smtp_code'])) {
4188
                    $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code'];
4189
                }
4190
                if (!empty($lasterror['smtp_code_ex'])) {
4191
                    $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex'];
4192
                }
4193
            }
4194
        }
4195
        $this->ErrorInfo = $msg;
4196
    }
4197
4198
    /**
4199
     * Return an RFC 822 formatted date.
4200
     *
4201
     * @return string
4202
     */
4203
    public static function rfcDate()
4204
    {
4205
        //Set the time zone to whatever the default is to avoid 500 errors
4206
        //Will default to UTC if it's not set properly in php.ini
4207
        date_default_timezone_set(@date_default_timezone_get());
4208
4209
        return date('D, j M Y H:i:s O');
4210
    }
4211
4212
    /**
4213
     * Get the server hostname.
4214
     * Returns 'localhost.localdomain' if unknown.
4215
     *
4216
     * @return string
4217
     */
4218
    protected function serverHostname()
4219
    {
4220
        $result = '';
4221
        if (!empty($this->Hostname)) {
4222
            $result = $this->Hostname;
4223
        } elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) {
4224
            $result = $_SERVER['SERVER_NAME'];
4225
        } elseif (function_exists('gethostname') && gethostname() !== false) {
4226
            $result = gethostname();
4227
        } elseif (php_uname('n') !== '') {
4228
            $result = php_uname('n');
4229
        }
4230
        if (!static::isValidHost($result)) {
4231
            return 'localhost.localdomain';
4232
        }
4233
4234
        return $result;
4235
    }
4236
4237
    /**
4238
     * Validate whether a string contains a valid value to use as a hostname or IP address.
4239
     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
4240
     *
4241
     * @param string $host The host name or IP address to check
4242
     *
4243
     * @return bool
4244
     */
4245
    public static function isValidHost($host)
4246
    {
4247
        //Simple syntax limits
4248
        if (
4249
            empty($host)
4250
            || !is_string($host)
4251
            || strlen($host) > 256
4252
            || !preg_match('/^([a-z\d.-]*|\[[a-f\d:]+\])$/i', $host)
4253
        ) {
4254
            return false;
4255
        }
4256
        //Looks like a bracketed IPv6 address
4257
        if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') {
4258
            return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
4259
        }
4260
        //If removing all the dots results in a numeric string, it must be an IPv4 address.
4261
        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
4262
        if (is_numeric(str_replace('.', '', $host))) {
4263
            //Is it a valid IPv4 address?
4264
            return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
4265
        }
4266
        //Is it a syntactically valid hostname (when embedded in a URL)?
4267
        return filter_var('https://' . $host, FILTER_VALIDATE_URL) !== false;
4268
    }
4269
4270
    /**
4271
     * Get an error message in the current language.
4272
     *
4273
     * @param string $key
4274
     *
4275
     * @return string
4276
     */
4277
    protected function lang($key)
4278
    {
4279
        if (count($this->language) < 1) {
4280
            $this->setLanguage(); //Set the default language
4281
        }
4282
4283
        if (array_key_exists($key, $this->language)) {
4284
            if ('smtp_connect_failed' === $key) {
4285
                //Include a link to troubleshooting docs on SMTP connection failure.
4286
                //This is by far the biggest cause of support questions
4287
                //but it's usually not PHPMailer's fault.
4288
                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
4289
            }
4290
4291
            return $this->language[$key];
4292
        }
4293
4294
        //Return the key as a fallback
4295
        return $key;
4296
    }
4297
4298
    /**
4299
     * Build an error message starting with a generic one and adding details if possible.
4300
     *
4301
     * @param string $base_key
4302
     * @return string
4303
     */
4304
    private function getSmtpErrorMessage($base_key)
4305
    {
4306
        $message = $this->lang($base_key);
4307
        $error = $this->smtp->getError();
4308
        if (!empty($error['error'])) {
4309
            $message .= ' ' . $error['error'];
4310
            if (!empty($error['detail'])) {
4311
                $message .= ' ' . $error['detail'];
4312
            }
4313
        }
4314
4315
        return $message;
4316
    }
4317
4318
    /**
4319
     * Check if an error occurred.
4320
     *
4321
     * @return bool True if an error did occur
4322
     */
4323
    public function isError()
4324
    {
4325
        return $this->error_count > 0;
4326
    }
4327
4328
    /**
4329
     * Add a custom header.
4330
     * $name value can be overloaded to contain
4331
     * both header name and value (name:value).
4332
     *
4333
     * @param string      $name  Custom header name
4334
     * @param string|null $value Header value
4335
     *
4336
     * @return bool True if a header was set successfully
4337
     * @throws Exception
4338
     */
4339
    public function addCustomHeader($name, $value = null)
4340
    {
4341
        if (null === $value && strpos($name, ':') !== false) {
4342
            //Value passed in as name:value
4343
            list($name, $value) = explode(':', $name, 2);
4344
        }
4345
        $name = trim($name);
4346
        $value = (null === $value) ? '' : trim($value);
4347
        //Ensure name is not empty, and that neither name nor value contain line breaks
4348
        if (empty($name) || strpbrk($name . $value, "\r\n") !== false) {
4349
            if ($this->exceptions) {
4350
                throw new Exception($this->lang('invalid_header'));
4351
            }
4352
4353
            return false;
4354
        }
4355
        $this->CustomHeader[] = [$name, $value];
4356
4357
        return true;
4358
    }
4359
4360
    /**
4361
     * Returns all custom headers.
4362
     *
4363
     * @return array
4364
     */
4365
    public function getCustomHeaders()
4366
    {
4367
        return $this->CustomHeader;
4368
    }
4369
4370
    /**
4371
     * Create a message body from an HTML string.
4372
     * Automatically inlines images and creates a plain-text version by converting the HTML,
4373
     * overwriting any existing values in Body and AltBody.
4374
     * Do not source $message content from user input!
4375
     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
4376
     * will look for an image file in $basedir/images/a.png and convert it to inline.
4377
     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
4378
     * Converts data-uri images into embedded attachments.
4379
     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
4380
     *
4381
     * @param string        $message  HTML message string
4382
     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
4383
     * @param bool|callable $advanced Whether to use the internal HTML to text converter
4384
     *                                or your own custom converter
4385
     * @return string The transformed message body
4386
     *
4387
     * @throws Exception
4388
     *
4389
     * @see PHPMailer::html2text()
4390
     */
4391
    public function msgHTML($message, $basedir = '', $advanced = false)
4392
    {
4393
        preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
4394
        if (array_key_exists(2, $images)) {
4395
            if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
4396
                //Ensure $basedir has a trailing /
4397
                $basedir .= '/';
4398
            }
4399
            foreach ($images[2] as $imgindex => $url) {
4400
                //Convert data URIs into embedded images
4401
                //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
4402
                $match = [];
4403
                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
4404
                    if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
4405
                        $data = base64_decode($match[3]);
4406
                    } elseif ('' === $match[2]) {
4407
                        $data = rawurldecode($match[3]);
4408
                    } else {
4409
                        //Not recognised so leave it alone
4410
                        continue;
4411
                    }
4412
                    //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
4413
                    //will only be embedded once, even if it used a different encoding
4414
                    $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; //RFC2392 S 2
4415
4416
                    if (!$this->cidExists($cid)) {
4417
                        $this->addStringEmbeddedImage(
4418
                            $data,
4419
                            $cid,
4420
                            'embed' . $imgindex,
4421
                            static::ENCODING_BASE64,
4422
                            $match[1]
4423
                        );
4424
                    }
4425
                    $message = str_replace(
4426
                        $images[0][$imgindex],
4427
                        $images[1][$imgindex] . '="cid:' . $cid . '"',
4428
                        $message
4429
                    );
4430
                    continue;
4431
                }
4432
                if (
4433
                    //Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
4434
                    !empty($basedir)
4435
                    //Ignore URLs containing parent dir traversal (..)
4436
                    && (strpos($url, '..') === false)
4437
                    //Do not change urls that are already inline images
4438
                    && 0 !== strpos($url, 'cid:')
4439
                    //Do not change absolute URLs, including anonymous protocol
4440
                    && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
4441
                ) {
4442
                    $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
4443
                    $directory = dirname($url);
4444
                    if ('.' === $directory) {
4445
                        $directory = '';
4446
                    }
4447
                    //RFC2392 S 2
4448
                    $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
4449
                    if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
4450
                        $basedir .= '/';
4451
                    }
4452
                    if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
4453
                        $directory .= '/';
4454
                    }
4455
                    if (
4456
                        $this->addEmbeddedImage(
4457
                            $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

4457
                            $basedir . $directory . /** @scrutinizer ignore-type */ $filename,
Loading history...
4458
                            $cid,
4459
                            $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

4459
                            /** @scrutinizer ignore-type */ $filename,
Loading history...
4460
                            static::ENCODING_BASE64,
4461
                            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

4461
                            static::_mime_types((string) static::mb_pathinfo(/** @scrutinizer ignore-type */ $filename, PATHINFO_EXTENSION))
Loading history...
4462
                        )
4463
                    ) {
4464
                        $message = preg_replace(
4465
                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
4466
                            $images[1][$imgindex] . '="cid:' . $cid . '"',
4467
                            $message
4468
                        );
4469
                    }
4470
                }
4471
            }
4472
        }
4473
        $this->isHTML();
4474
        //Convert all message body line breaks to LE, makes quoted-printable encoding work much better
4475
        $this->Body = static::normalizeBreaks($message);
4476
        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
4477
        if (!$this->alternativeExists()) {
4478
            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
4479
                . static::$LE;
4480
        }
4481
4482
        return $this->Body;
4483
    }
4484
4485
    /**
4486
     * Convert an HTML string into plain text.
4487
     * This is used by msgHTML().
4488
     * Note - older versions of this function used a bundled advanced converter
4489
     * which was removed for license reasons in #232.
4490
     * Example usage:
4491
     *
4492
     * ```php
4493
     * //Use default conversion
4494
     * $plain = $mail->html2text($html);
4495
     * //Use your own custom converter
4496
     * $plain = $mail->html2text($html, function($html) {
4497
     *     $converter = new MyHtml2text($html);
4498
     *     return $converter->get_text();
4499
     * });
4500
     * ```
4501
     *
4502
     * @param string        $html     The HTML text to convert
4503
     * @param bool|callable $advanced Any boolean value to use the internal converter,
4504
     *                                or provide your own callable for custom conversion.
4505
     *                                *Never* pass user-supplied data into this parameter
4506
     *
4507
     * @return string
4508
     */
4509
    public function html2text($html, $advanced = false)
4510
    {
4511
        if (is_callable($advanced)) {
4512
            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

4512
            return call_user_func(/** @scrutinizer ignore-type */ $advanced, $html);
Loading history...
4513
        }
4514
4515
        return html_entity_decode(
4516
            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
4517
            ENT_QUOTES,
4518
            $this->CharSet
4519
        );
4520
    }
4521
4522
    /**
4523
     * Get the MIME type for a file extension.
4524
     *
4525
     * @param string $ext File extension
4526
     *
4527
     * @return string MIME type of file
4528
     */
4529
    public static function _mime_types($ext = '')
4530
    {
4531
        $mimes = [
4532
            'xl' => 'application/excel',
4533
            'js' => 'application/javascript',
4534
            'hqx' => 'application/mac-binhex40',
4535
            'cpt' => 'application/mac-compactpro',
4536
            'bin' => 'application/macbinary',
4537
            'doc' => 'application/msword',
4538
            'word' => 'application/msword',
4539
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
4540
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
4541
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
4542
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
4543
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
4544
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
4545
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
4546
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
4547
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
4548
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
4549
            'class' => 'application/octet-stream',
4550
            'dll' => 'application/octet-stream',
4551
            'dms' => 'application/octet-stream',
4552
            'exe' => 'application/octet-stream',
4553
            'lha' => 'application/octet-stream',
4554
            'lzh' => 'application/octet-stream',
4555
            'psd' => 'application/octet-stream',
4556
            'sea' => 'application/octet-stream',
4557
            'so' => 'application/octet-stream',
4558
            'oda' => 'application/oda',
4559
            'pdf' => 'application/pdf',
4560
            'ai' => 'application/postscript',
4561
            'eps' => 'application/postscript',
4562
            'ps' => 'application/postscript',
4563
            'smi' => 'application/smil',
4564
            'smil' => 'application/smil',
4565
            'mif' => 'application/vnd.mif',
4566
            'xls' => 'application/vnd.ms-excel',
4567
            'ppt' => 'application/vnd.ms-powerpoint',
4568
            'wbxml' => 'application/vnd.wap.wbxml',
4569
            'wmlc' => 'application/vnd.wap.wmlc',
4570
            'dcr' => 'application/x-director',
4571
            'dir' => 'application/x-director',
4572
            'dxr' => 'application/x-director',
4573
            'dvi' => 'application/x-dvi',
4574
            'gtar' => 'application/x-gtar',
4575
            'php3' => 'application/x-httpd-php',
4576
            'php4' => 'application/x-httpd-php',
4577
            'php' => 'application/x-httpd-php',
4578
            'phtml' => 'application/x-httpd-php',
4579
            'phps' => 'application/x-httpd-php-source',
4580
            'swf' => 'application/x-shockwave-flash',
4581
            'sit' => 'application/x-stuffit',
4582
            'tar' => 'application/x-tar',
4583
            'tgz' => 'application/x-tar',
4584
            'xht' => 'application/xhtml+xml',
4585
            'xhtml' => 'application/xhtml+xml',
4586
            'zip' => 'application/zip',
4587
            'mid' => 'audio/midi',
4588
            'midi' => 'audio/midi',
4589
            'mp2' => 'audio/mpeg',
4590
            'mp3' => 'audio/mpeg',
4591
            'm4a' => 'audio/mp4',
4592
            'mpga' => 'audio/mpeg',
4593
            'aif' => 'audio/x-aiff',
4594
            'aifc' => 'audio/x-aiff',
4595
            'aiff' => 'audio/x-aiff',
4596
            'ram' => 'audio/x-pn-realaudio',
4597
            'rm' => 'audio/x-pn-realaudio',
4598
            'rpm' => 'audio/x-pn-realaudio-plugin',
4599
            'ra' => 'audio/x-realaudio',
4600
            'wav' => 'audio/x-wav',
4601
            'mka' => 'audio/x-matroska',
4602
            'bmp' => 'image/bmp',
4603
            'gif' => 'image/gif',
4604
            'jpeg' => 'image/jpeg',
4605
            'jpe' => 'image/jpeg',
4606
            'jpg' => 'image/jpeg',
4607
            'png' => 'image/png',
4608
            'tiff' => 'image/tiff',
4609
            'tif' => 'image/tiff',
4610
            'webp' => 'image/webp',
4611
            'avif' => 'image/avif',
4612
            'heif' => 'image/heif',
4613
            'heifs' => 'image/heif-sequence',
4614
            'heic' => 'image/heic',
4615
            'heics' => 'image/heic-sequence',
4616
            'eml' => 'message/rfc822',
4617
            'css' => 'text/css',
4618
            'html' => 'text/html',
4619
            'htm' => 'text/html',
4620
            'shtml' => 'text/html',
4621
            'log' => 'text/plain',
4622
            'text' => 'text/plain',
4623
            'txt' => 'text/plain',
4624
            'rtx' => 'text/richtext',
4625
            'rtf' => 'text/rtf',
4626
            'vcf' => 'text/vcard',
4627
            'vcard' => 'text/vcard',
4628
            'ics' => 'text/calendar',
4629
            'xml' => 'text/xml',
4630
            'xsl' => 'text/xml',
4631
            'csv' => 'text/csv',
4632
            'wmv' => 'video/x-ms-wmv',
4633
            'mpeg' => 'video/mpeg',
4634
            'mpe' => 'video/mpeg',
4635
            'mpg' => 'video/mpeg',
4636
            'mp4' => 'video/mp4',
4637
            'm4v' => 'video/mp4',
4638
            'mov' => 'video/quicktime',
4639
            'qt' => 'video/quicktime',
4640
            'rv' => 'video/vnd.rn-realvideo',
4641
            'avi' => 'video/x-msvideo',
4642
            'movie' => 'video/x-sgi-movie',
4643
            'webm' => 'video/webm',
4644
            'mkv' => 'video/x-matroska',
4645
        ];
4646
        $ext = strtolower($ext);
4647
        if (array_key_exists($ext, $mimes)) {
4648
            return $mimes[$ext];
4649
        }
4650
4651
        return 'application/octet-stream';
4652
    }
4653
4654
    /**
4655
     * Map a file name to a MIME type.
4656
     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
4657
     *
4658
     * @param string $filename A file name or full path, does not need to exist as a file
4659
     *
4660
     * @return string
4661
     */
4662
    public static function filenameToType($filename)
4663
    {
4664
        //In case the path is a URL, strip any query string before getting extension
4665
        $qpos = strpos($filename, '?');
4666
        if (false !== $qpos) {
4667
            $filename = substr($filename, 0, $qpos);
4668
        }
4669
        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
4670
4671
        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

4671
        return static::_mime_types(/** @scrutinizer ignore-type */ $ext);
Loading history...
4672
    }
4673
4674
    /**
4675
     * Multi-byte-safe pathinfo replacement.
4676
     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
4677
     *
4678
     * @see https://www.php.net/manual/en/function.pathinfo.php#107461
4679
     *
4680
     * @param string     $path    A filename or path, does not need to exist as a file
4681
     * @param int|string $options Either a PATHINFO_* constant,
4682
     *                            or a string name to return only the specified piece
4683
     *
4684
     * @return string|array
4685
     */
4686
    public static function mb_pathinfo($path, $options = null)
4687
    {
4688
        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
4689
        $pathinfo = [];
4690
        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
4691
            if (array_key_exists(1, $pathinfo)) {
4692
                $ret['dirname'] = $pathinfo[1];
4693
            }
4694
            if (array_key_exists(2, $pathinfo)) {
4695
                $ret['basename'] = $pathinfo[2];
4696
            }
4697
            if (array_key_exists(5, $pathinfo)) {
4698
                $ret['extension'] = $pathinfo[5];
4699
            }
4700
            if (array_key_exists(3, $pathinfo)) {
4701
                $ret['filename'] = $pathinfo[3];
4702
            }
4703
        }
4704
        switch ($options) {
4705
            case PATHINFO_DIRNAME:
4706
            case 'dirname':
4707
                return $ret['dirname'];
4708
            case PATHINFO_BASENAME:
4709
            case 'basename':
4710
                return $ret['basename'];
4711
            case PATHINFO_EXTENSION:
4712
            case 'extension':
4713
                return $ret['extension'];
4714
            case PATHINFO_FILENAME:
4715
            case 'filename':
4716
                return $ret['filename'];
4717
            default:
4718
                return $ret;
4719
        }
4720
    }
4721
4722
    /**
4723
     * Set or reset instance properties.
4724
     * You should avoid this function - it's more verbose, less efficient, more error-prone and
4725
     * harder to debug than setting properties directly.
4726
     * Usage Example:
4727
     * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`
4728
     *   is the same as:
4729
     * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.
4730
     *
4731
     * @param string $name  The property name to set
4732
     * @param mixed  $value The value to set the property to
4733
     *
4734
     * @return bool
4735
     */
4736
    public function set($name, $value = '')
4737
    {
4738
        if (property_exists($this, $name)) {
4739
            $this->{$name} = $value;
4740
4741
            return true;
4742
        }
4743
        $this->setError($this->lang('variable_set') . $name);
4744
4745
        return false;
4746
    }
4747
4748
    /**
4749
     * Strip newlines to prevent header injection.
4750
     *
4751
     * @param string $str
4752
     *
4753
     * @return string
4754
     */
4755
    public function secureHeader($str)
4756
    {
4757
        return trim(str_replace(["\r", "\n"], '', $str));
4758
    }
4759
4760
    /**
4761
     * Normalize line breaks in a string.
4762
     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
4763
     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
4764
     *
4765
     * @param string $text
4766
     * @param string $breaktype What kind of line break to use; defaults to static::$LE
4767
     *
4768
     * @return string
4769
     */
4770
    public static function normalizeBreaks($text, $breaktype = null)
4771
    {
4772
        if (null === $breaktype) {
4773
            $breaktype = static::$LE;
4774
        }
4775
        //Normalise to \n
4776
        $text = str_replace([self::CRLF, "\r"], "\n", $text);
4777
        //Now convert LE as needed
4778
        if ("\n" !== $breaktype) {
4779
            $text = str_replace("\n", $breaktype, $text);
4780
        }
4781
4782
        return $text;
4783
    }
4784
4785
    /**
4786
     * Remove trailing whitespace from a string.
4787
     *
4788
     * @param string $text
4789
     *
4790
     * @return string The text to remove whitespace from
4791
     */
4792
    public static function stripTrailingWSP($text)
4793
    {
4794
        return rtrim($text, " \r\n\t");
4795
    }
4796
4797
    /**
4798
     * Strip trailing line breaks from a string.
4799
     *
4800
     * @param string $text
4801
     *
4802
     * @return string The text to remove breaks from
4803
     */
4804
    public static function stripTrailingBreaks($text)
4805
    {
4806
        return rtrim($text, "\r\n");
4807
    }
4808
4809
    /**
4810
     * Return the current line break format string.
4811
     *
4812
     * @return string
4813
     */
4814
    public static function getLE()
4815
    {
4816
        return static::$LE;
4817
    }
4818
4819
    /**
4820
     * Set the line break format string, e.g. "\r\n".
4821
     *
4822
     * @param string $le
4823
     */
4824
    protected static function setLE($le)
4825
    {
4826
        static::$LE = $le;
4827
    }
4828
4829
    /**
4830
     * Set the public and private key files and password for S/MIME signing.
4831
     *
4832
     * @param string $cert_filename
4833
     * @param string $key_filename
4834
     * @param string $key_pass            Password for private key
4835
     * @param string $extracerts_filename Optional path to chain certificate
4836
     */
4837
    public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
4838
    {
4839
        $this->sign_cert_file = $cert_filename;
4840
        $this->sign_key_file = $key_filename;
4841
        $this->sign_key_pass = $key_pass;
4842
        $this->sign_extracerts_file = $extracerts_filename;
4843
    }
4844
4845
    /**
4846
     * Quoted-Printable-encode a DKIM header.
4847
     *
4848
     * @param string $txt
4849
     *
4850
     * @return string
4851
     */
4852
    public function DKIM_QP($txt)
4853
    {
4854
        $line = '';
4855
        $len = strlen($txt);
4856
        for ($i = 0; $i < $len; ++$i) {
4857
            $ord = ord($txt[$i]);
4858
            if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
4859
                $line .= $txt[$i];
4860
            } else {
4861
                $line .= '=' . sprintf('%02X', $ord);
4862
            }
4863
        }
4864
4865
        return $line;
4866
    }
4867
4868
    /**
4869
     * Generate a DKIM signature.
4870
     *
4871
     * @param string $signHeader
4872
     *
4873
     * @throws Exception
4874
     *
4875
     * @return string The DKIM signature value
4876
     */
4877
    public function DKIM_Sign($signHeader)
4878
    {
4879
        if (!defined('PKCS7_TEXT')) {
4880
            if ($this->exceptions) {
4881
                throw new Exception($this->lang('extension_missing') . 'openssl');
4882
            }
4883
4884
            return '';
4885
        }
4886
        $privKeyStr = !empty($this->DKIM_private_string) ?
4887
            $this->DKIM_private_string :
4888
            file_get_contents($this->DKIM_private);
4889
        if ('' !== $this->DKIM_passphrase) {
4890
            $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
4891
        } else {
4892
            $privKey = openssl_pkey_get_private($privKeyStr);
4893
        }
4894
        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
4895
            if (\PHP_MAJOR_VERSION < 8) {
4896
                openssl_pkey_free($privKey);
4897
            }
4898
4899
            return base64_encode($signature);
4900
        }
4901
        if (\PHP_MAJOR_VERSION < 8) {
4902
            openssl_pkey_free($privKey);
4903
        }
4904
4905
        return '';
4906
    }
4907
4908
    /**
4909
     * Generate a DKIM canonicalization header.
4910
     * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
4911
     * Canonicalized headers should *always* use CRLF, regardless of mailer setting.
4912
     *
4913
     * @see https://www.rfc-editor.org/rfc/rfc6376#section-3.4.2
4914
     *
4915
     * @param string $signHeader Header
4916
     *
4917
     * @return string
4918
     */
4919
    public function DKIM_HeaderC($signHeader)
4920
    {
4921
        //Normalize breaks to CRLF (regardless of the mailer)
4922
        $signHeader = static::normalizeBreaks($signHeader, self::CRLF);
4923
        //Unfold header lines
4924
        //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
4925
        //@see https://www.rfc-editor.org/rfc/rfc5322#section-2.2
4926
        //That means this may break if you do something daft like put vertical tabs in your headers.
4927
        $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
4928
        //Break headers out into an array
4929
        $lines = explode(self::CRLF, $signHeader);
4930
        foreach ($lines as $key => $line) {
4931
            //If the header is missing a :, skip it as it's invalid
4932
            //This is likely to happen because the explode() above will also split
4933
            //on the trailing LE, leaving an empty line
4934
            if (strpos($line, ':') === false) {
4935
                continue;
4936
            }
4937
            list($heading, $value) = explode(':', $line, 2);
4938
            //Lower-case header name
4939
            $heading = strtolower($heading);
4940
            //Collapse white space within the value, also convert WSP to space
4941
            $value = preg_replace('/[ \t]+/', ' ', $value);
4942
            //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
4943
            //But then says to delete space before and after the colon.
4944
            //Net result is the same as trimming both ends of the value.
4945
            //By elimination, the same applies to the field name
4946
            $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
4947
        }
4948
4949
        return implode(self::CRLF, $lines);
4950
    }
4951
4952
    /**
4953
     * Generate a DKIM canonicalization body.
4954
     * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
4955
     * Canonicalized bodies should *always* use CRLF, regardless of mailer setting.
4956
     *
4957
     * @see https://www.rfc-editor.org/rfc/rfc6376#section-3.4.3
4958
     *
4959
     * @param string $body Message Body
4960
     *
4961
     * @return string
4962
     */
4963
    public function DKIM_BodyC($body)
4964
    {
4965
        if (empty($body)) {
4966
            return self::CRLF;
4967
        }
4968
        //Normalize line endings to CRLF
4969
        $body = static::normalizeBreaks($body, self::CRLF);
4970
4971
        //Reduce multiple trailing line breaks to a single one
4972
        return static::stripTrailingBreaks($body) . self::CRLF;
4973
    }
4974
4975
    /**
4976
     * Create the DKIM header and body in a new message header.
4977
     *
4978
     * @param string $headers_line Header lines
4979
     * @param string $subject      Subject
4980
     * @param string $body         Body
4981
     *
4982
     * @throws Exception
4983
     *
4984
     * @return string
4985
     */
4986
    public function DKIM_Add($headers_line, $subject, $body)
4987
    {
4988
        $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms
4989
        $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body
4990
        $DKIMquery = 'dns/txt'; //Query method
4991
        $DKIMtime = time();
4992
        //Always sign these headers without being asked
4993
        //Recommended list from https://www.rfc-editor.org/rfc/rfc6376#section-5.4.1
4994
        $autoSignHeaders = [
4995
            'from',
4996
            'to',
4997
            'cc',
4998
            'date',
4999
            'subject',
5000
            'reply-to',
5001
            'message-id',
5002
            'content-type',
5003
            'mime-version',
5004
            'x-mailer',
5005
        ];
5006
        if (stripos($headers_line, 'Subject') === false) {
5007
            $headers_line .= 'Subject: ' . $subject . static::$LE;
5008
        }
5009
        $headerLines = explode(static::$LE, $headers_line);
5010
        $currentHeaderLabel = '';
5011
        $currentHeaderValue = '';
5012
        $parsedHeaders = [];
5013
        $headerLineIndex = 0;
5014
        $headerLineCount = count($headerLines);
5015
        foreach ($headerLines as $headerLine) {
5016
            $matches = [];
5017
            if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
5018
                if ($currentHeaderLabel !== '') {
5019
                    //We were previously in another header; This is the start of a new header, so save the previous one
5020
                    $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
5021
                }
5022
                $currentHeaderLabel = $matches[1];
5023
                $currentHeaderValue = $matches[2];
5024
            } elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
5025
                //This is a folded continuation of the current header, so unfold it
5026
                $currentHeaderValue .= ' ' . $matches[1];
5027
            }
5028
            ++$headerLineIndex;
5029
            if ($headerLineIndex >= $headerLineCount) {
5030
                //This was the last line, so finish off this header
5031
                $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
5032
            }
5033
        }
5034
        $copiedHeaders = [];
5035
        $headersToSignKeys = [];
5036
        $headersToSign = [];
5037
        foreach ($parsedHeaders as $header) {
5038
            //Is this header one that must be included in the DKIM signature?
5039
            if (in_array(strtolower($header['label']), $autoSignHeaders, true)) {
5040
                $headersToSignKeys[] = $header['label'];
5041
                $headersToSign[] = $header['label'] . ': ' . $header['value'];
5042
                if ($this->DKIM_copyHeaderFields) {
5043
                    $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
5044
                        str_replace('|', '=7C', $this->DKIM_QP($header['value']));
5045
                }
5046
                continue;
5047
            }
5048
            //Is this an extra custom header we've been asked to sign?
5049
            if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {
5050
                //Find its value in custom headers
5051
                foreach ($this->CustomHeader as $customHeader) {
5052
                    if ($customHeader[0] === $header['label']) {
5053
                        $headersToSignKeys[] = $header['label'];
5054
                        $headersToSign[] = $header['label'] . ': ' . $header['value'];
5055
                        if ($this->DKIM_copyHeaderFields) {
5056
                            $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
5057
                                str_replace('|', '=7C', $this->DKIM_QP($header['value']));
5058
                        }
5059
                        //Skip straight to the next header
5060
                        continue 2;
5061
                    }
5062
                }
5063
            }
5064
        }
5065
        $copiedHeaderFields = '';
5066
        if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {
5067
            //Assemble a DKIM 'z' tag
5068
            $copiedHeaderFields = ' z=';
5069
            $first = true;
5070
            foreach ($copiedHeaders as $copiedHeader) {
5071
                if (!$first) {
5072
                    $copiedHeaderFields .= static::$LE . ' |';
5073
                }
5074
                //Fold long values
5075
                if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {
5076
                    $copiedHeaderFields .= substr(
5077
                        chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS),
5078
                        0,
5079
                        -strlen(static::$LE . self::FWS)
5080
                    );
5081
                } else {
5082
                    $copiedHeaderFields .= $copiedHeader;
5083
                }
5084
                $first = false;
5085
            }
5086
            $copiedHeaderFields .= ';' . static::$LE;
5087
        }
5088
        $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
5089
        $headerValues = implode(static::$LE, $headersToSign);
5090
        $body = $this->DKIM_BodyC($body);
5091
        //Base64 of packed binary SHA-256 hash of body
5092
        $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body)));
5093
        $ident = '';
5094
        if ('' !== $this->DKIM_identity) {
5095
            $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
5096
        }
5097
        //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
5098
        //which is appended after calculating the signature
5099
        //https://www.rfc-editor.org/rfc/rfc6376#section-3.5
5100
        $dkimSignatureHeader = 'DKIM-Signature: v=1;' .
5101
            ' d=' . $this->DKIM_domain . ';' .
5102
            ' s=' . $this->DKIM_selector . ';' . static::$LE .
5103
            ' a=' . $DKIMsignatureType . ';' .
5104
            ' q=' . $DKIMquery . ';' .
5105
            ' t=' . $DKIMtime . ';' .
5106
            ' c=' . $DKIMcanonicalization . ';' . static::$LE .
5107
            $headerKeys .
5108
            $ident .
5109
            $copiedHeaderFields .
5110
            ' bh=' . $DKIMb64 . ';' . static::$LE .
5111
            ' b=';
5112
        //Canonicalize the set of headers
5113
        $canonicalizedHeaders = $this->DKIM_HeaderC(
5114
            $headerValues . static::$LE . $dkimSignatureHeader
5115
        );
5116
        $signature = $this->DKIM_Sign($canonicalizedHeaders);
5117
        $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS));
5118
5119
        return static::normalizeBreaks($dkimSignatureHeader . $signature);
5120
    }
5121
5122
    /**
5123
     * Detect if a string contains a line longer than the maximum line length
5124
     * allowed by RFC 2822 section 2.1.1.
5125
     *
5126
     * @param string $str
5127
     *
5128
     * @return bool
5129
     */
5130
    public static function hasLineLongerThanMax($str)
5131
    {
5132
        return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
5133
    }
5134
5135
    /**
5136
     * If a string contains any "special" characters, double-quote the name,
5137
     * and escape any double quotes with a backslash.
5138
     *
5139
     * @param string $str
5140
     *
5141
     * @return string
5142
     *
5143
     * @see RFC822 3.4.1
5144
     */
5145
    public static function quotedString($str)
5146
    {
5147
        if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) {
5148
            //If the string contains any of these chars, it must be double-quoted
5149
            //and any double quotes must be escaped with a backslash
5150
            return '"' . str_replace('"', '\\"', $str) . '"';
5151
        }
5152
5153
        //Return the string untouched, it doesn't need quoting
5154
        return $str;
5155
    }
5156
5157
    /**
5158
     * Allows for public read access to 'to' property.
5159
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5160
     *
5161
     * @return array
5162
     */
5163
    public function getToAddresses()
5164
    {
5165
        return $this->to;
5166
    }
5167
5168
    /**
5169
     * Allows for public read access to 'cc' property.
5170
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5171
     *
5172
     * @return array
5173
     */
5174
    public function getCcAddresses()
5175
    {
5176
        return $this->cc;
5177
    }
5178
5179
    /**
5180
     * Allows for public read access to 'bcc' property.
5181
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5182
     *
5183
     * @return array
5184
     */
5185
    public function getBccAddresses()
5186
    {
5187
        return $this->bcc;
5188
    }
5189
5190
    /**
5191
     * Allows for public read access to 'ReplyTo' property.
5192
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5193
     *
5194
     * @return array
5195
     */
5196
    public function getReplyToAddresses()
5197
    {
5198
        return $this->ReplyTo;
5199
    }
5200
5201
    /**
5202
     * Allows for public read access to 'all_recipients' property.
5203
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
5204
     *
5205
     * @return array
5206
     */
5207
    public function getAllRecipientAddresses()
5208
    {
5209
        return $this->all_recipients;
5210
    }
5211
5212
    /**
5213
     * Perform a callback.
5214
     *
5215
     * @param bool   $isSent
5216
     * @param array  $to
5217
     * @param array  $cc
5218
     * @param array  $bcc
5219
     * @param string $subject
5220
     * @param string $body
5221
     * @param string $from
5222
     * @param array  $extra
5223
     */
5224
    protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
5225
    {
5226
        if (!empty($this->action_function) && is_callable($this->action_function)) {
5227
            call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);
5228
        }
5229
    }
5230
5231
    /**
5232
     * Get the OAuthTokenProvider instance.
5233
     *
5234
     * @return OAuthTokenProvider
5235
     */
5236
    public function getOAuth()
5237
    {
5238
        return $this->oauth;
5239
    }
5240
5241
    /**
5242
     * Set an OAuthTokenProvider instance.
5243
     */
5244
    public function setOAuth(OAuthTokenProvider $oauth)
5245
    {
5246
        $this->oauth = $oauth;
5247
    }
5248
}
5249