Passed
Push — master ( a46cb4...8ff499 )
by Michael
27:12 queued 15:45
created

PHPMailer::getMailMIME()   B

Complexity

Conditions 11
Paths 32

Size

Total Lines 41
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 30
nc 32
nop 0
dl 0
loc 41
rs 7.3166
c 1
b 0
f 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * PHPMailer - PHP email creation and transport class.
5
 * PHP Version 5.5.
6
 *
7
 * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
8
 *
9
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
10
 * @author    Jim Jagielski (jimjag) <[email protected]>
11
 * @author    Andy Prevost (codeworxtech) <[email protected]>
12
 * @author    Brent R. Matzelle (original founder)
13
 * @copyright 2012 - 2020 Marcus Bointon
14
 * @copyright 2010 - 2012 Jim Jagielski
15
 * @copyright 2004 - 2009 Andy Prevost
16
 * @license   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
     * If CharSet is UTF8, the validator is left at the default value,
584
     * and you send to addresses that use non-ASCII local parts, then
585
     * PHPMailer automatically changes to the 'eai' validator.
586
     *
587
     * @see PHPMailer::validateAddress()
588
     *
589
     * @var string|callable
590
     */
591
    public static $validator = 'php';
592
593
    /**
594
     * An instance of the SMTP sender class.
595
     *
596
     * @var SMTP
597
     */
598
    protected $smtp;
599
600
    /**
601
     * The array of 'to' names and addresses.
602
     *
603
     * @var array
604
     */
605
    protected $to = [];
606
607
    /**
608
     * The array of 'cc' names and addresses.
609
     *
610
     * @var array
611
     */
612
    protected $cc = [];
613
614
    /**
615
     * The array of 'bcc' names and addresses.
616
     *
617
     * @var array
618
     */
619
    protected $bcc = [];
620
621
    /**
622
     * The array of reply-to names and addresses.
623
     *
624
     * @var array
625
     */
626
    protected $ReplyTo = [];
627
628
    /**
629
     * An array of all kinds of addresses.
630
     * Includes all of $to, $cc, $bcc.
631
     *
632
     * @see PHPMailer::$to
633
     * @see PHPMailer::$cc
634
     * @see PHPMailer::$bcc
635
     *
636
     * @var array
637
     */
638
    protected $all_recipients = [];
639
640
    /**
641
     * An array of names and addresses queued for validation.
642
     * In send(), valid and non duplicate entries are moved to $all_recipients
643
     * and one of $to, $cc, or $bcc.
644
     * This array is used only for addresses with IDN.
645
     *
646
     * @see PHPMailer::$to
647
     * @see PHPMailer::$cc
648
     * @see PHPMailer::$bcc
649
     * @see PHPMailer::$all_recipients
650
     *
651
     * @var array
652
     */
653
    protected $RecipientsQueue = [];
654
655
    /**
656
     * An array of reply-to names and addresses queued for validation.
657
     * In send(), valid and non duplicate entries are moved to $ReplyTo.
658
     * This array is used only for addresses with IDN.
659
     *
660
     * @see PHPMailer::$ReplyTo
661
     *
662
     * @var array
663
     */
664
    protected $ReplyToQueue = [];
665
666
    /**
667
     * Whether the need for SMTPUTF8 has been detected. Set by
668
     * preSend() if necessary.
669
     *
670
     * @var bool
671
     */
672
    public $UseSMTPUTF8 = false;
673
674
    /**
675
     * The array of attachments.
676
     *
677
     * @var array
678
     */
679
    protected $attachment = [];
680
681
    /**
682
     * The array of custom headers.
683
     *
684
     * @var array
685
     */
686
    protected $CustomHeader = [];
687
688
    /**
689
     * The most recent Message-ID (including angular brackets).
690
     *
691
     * @var string
692
     */
693
    protected $lastMessageID = '';
694
695
    /**
696
     * The message's MIME type.
697
     *
698
     * @var string
699
     */
700
    protected $message_type = '';
701
702
    /**
703
     * The array of MIME boundary strings.
704
     *
705
     * @var array
706
     */
707
    protected $boundary = [];
708
709
    /**
710
     * The array of available text strings for the current language.
711
     *
712
     * @var array
713
     */
714
    protected $language = [];
715
716
    /**
717
     * The number of errors encountered.
718
     *
719
     * @var int
720
     */
721
    protected $error_count = 0;
722
723
    /**
724
     * The S/MIME certificate file path.
725
     *
726
     * @var string
727
     */
728
    protected $sign_cert_file = '';
729
730
    /**
731
     * The S/MIME key file path.
732
     *
733
     * @var string
734
     */
735
    protected $sign_key_file = '';
736
737
    /**
738
     * The optional S/MIME extra certificates ("CA Chain") file path.
739
     *
740
     * @var string
741
     */
742
    protected $sign_extracerts_file = '';
743
744
    /**
745
     * The S/MIME password for the key.
746
     * Used only if the key is encrypted.
747
     *
748
     * @var string
749
     */
750
    protected $sign_key_pass = '';
751
752
    /**
753
     * Whether to throw exceptions for errors.
754
     *
755
     * @var bool
756
     */
757
    protected $exceptions = false;
758
759
    /**
760
     * Unique ID used for message ID and boundaries.
761
     *
762
     * @var string
763
     */
764
    protected $uniqueid = '';
765
766
    /**
767
     * The PHPMailer Version number.
768
     *
769
     * @var string
770
     */
771
    const VERSION = '6.10.0';
772
773
    /**
774
     * Error severity: message only, continue processing.
775
     *
776
     * @var int
777
     */
778
    const STOP_MESSAGE = 0;
779
780
    /**
781
     * Error severity: message, likely ok to continue processing.
782
     *
783
     * @var int
784
     */
785
    const STOP_CONTINUE = 1;
786
787
    /**
788
     * Error severity: message, plus full stop, critical error reached.
789
     *
790
     * @var int
791
     */
792
    const STOP_CRITICAL = 2;
793
794
    /**
795
     * The SMTP standard CRLF line break.
796
     * If you want to change line break format, change static::$LE, not this.
797
     */
798
    const CRLF = "\r\n";
799
800
    /**
801
     * "Folding White Space" a white space string used for line folding.
802
     */
803
    const FWS = ' ';
804
805
    /**
806
     * SMTP RFC standard line ending; Carriage Return, Line Feed.
807
     *
808
     * @var string
809
     */
810
    protected static $LE = self::CRLF;
811
812
    /**
813
     * The maximum line length supported by mail().
814
     *
815
     * Background: mail() will sometimes corrupt messages
816
     * with headers longer than 65 chars, see #818.
817
     *
818
     * @var int
819
     */
820
    const MAIL_MAX_LINE_LENGTH = 63;
821
822
    /**
823
     * The maximum line length allowed by RFC 2822 section 2.1.1.
824
     *
825
     * @var int
826
     */
827
    const MAX_LINE_LENGTH = 998;
828
829
    /**
830
     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
831
     * This length does NOT include the line break
832
     * 76 means that lines will be 77 or 78 chars depending on whether
833
     * the line break format is LF or CRLF; both are valid.
834
     *
835
     * @var int
836
     */
837
    const STD_LINE_LENGTH = 76;
838
839
    /**
840
     * Constructor.
841
     *
842
     * @param bool $exceptions Should we throw external exceptions?
843
     */
844
    public function __construct($exceptions = null)
845
    {
846
        if (null !== $exceptions) {
847
            $this->exceptions = (bool) $exceptions;
848
        }
849
        //Pick an appropriate debug output format automatically
850
        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
851
    }
852
853
    /**
854
     * Destructor.
855
     */
856
    public function __destruct()
857
    {
858
        //Close any open SMTP connection nicely
859
        $this->smtpClose();
860
    }
861
862
    /**
863
     * Call mail() in a safe_mode-aware fashion.
864
     * Also, unless sendmail_path points to sendmail (or something that
865
     * claims to be sendmail), don't pass params (not a perfect fix,
866
     * but it will do).
867
     *
868
     * @param string      $to      To
869
     * @param string      $subject Subject
870
     * @param string      $body    Message Body
871
     * @param string      $header  Additional Header(s)
872
     * @param string|null $params  Params
873
     *
874
     * @return bool
875
     */
876
    private function mailPassthru($to, $subject, $body, $header, $params)
877
    {
878
        //Check overloading of mail function to avoid double-encoding
879
        if ((int)ini_get('mbstring.func_overload') & 1) {
880
            $subject = $this->secureHeader($subject);
881
        } else {
882
            $subject = $this->encodeHeader($this->secureHeader($subject));
883
        }
884
        //Calling mail() with null params breaks
885
        $this->edebug('Sending with mail()');
886
        $this->edebug('Sendmail path: ' . ini_get('sendmail_path'));
887
        $this->edebug("Envelope sender: {$this->Sender}");
888
        $this->edebug("To: {$to}");
889
        $this->edebug("Subject: {$subject}");
890
        $this->edebug("Headers: {$header}");
891
        if (!$this->UseSendmailOptions || null === $params) {
892
            $result = @mail($to, $subject, $body, $header);
893
        } else {
894
            $this->edebug("Additional params: {$params}");
895
            $result = @mail($to, $subject, $body, $header, $params);
896
        }
897
        $this->edebug('Result: ' . ($result ? 'true' : 'false'));
898
        return $result;
899
    }
900
901
    /**
902
     * Output debugging info via a user-defined method.
903
     * Only generates output if debug output is enabled.
904
     *
905
     * @see PHPMailer::$Debugoutput
906
     * @see PHPMailer::$SMTPDebug
907
     *
908
     * @param string $str
909
     */
910
    protected function edebug($str)
911
    {
912
        if ($this->SMTPDebug <= 0) {
913
            return;
914
        }
915
        //Is this a PSR-3 logger?
916
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
917
            $this->Debugoutput->debug(rtrim($str, "\r\n"));
918
919
            return;
920
        }
921
        //Avoid clash with built-in function names
922
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
923
            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
924
925
            return;
926
        }
927
        switch ($this->Debugoutput) {
928
            case 'error_log':
929
                //Don't output, just log
930
                /** @noinspection ForgottenDebugOutputInspection */
931
                error_log($str);
932
                break;
933
            case 'html':
934
                //Cleans up output a bit for a better looking, HTML-safe output
935
                echo htmlentities(
936
                    preg_replace('/[\r\n]+/', '', $str),
937
                    ENT_QUOTES,
938
                    'UTF-8'
939
                ), "<br>\n";
940
                break;
941
            case 'echo':
942
            default:
943
                //Normalize line breaks
944
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
945
                echo gmdate('Y-m-d H:i:s'),
946
                "\t",
947
                    //Trim trailing space
948
                trim(
949
                    //Indent for readability, except for trailing break
950
                    str_replace(
951
                        "\n",
952
                        "\n                   \t                  ",
953
                        trim($str)
954
                    )
955
                ),
956
                "\n";
957
        }
958
    }
959
960
    /**
961
     * Sets message type to HTML or plain.
962
     *
963
     * @param bool $isHtml True for HTML mode
964
     */
965
    public function isHTML($isHtml = true)
966
    {
967
        if ($isHtml) {
968
            $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
969
        } else {
970
            $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
971
        }
972
    }
973
974
    /**
975
     * Send messages using SMTP.
976
     */
977
    public function isSMTP()
978
    {
979
        $this->Mailer = 'smtp';
980
    }
981
982
    /**
983
     * Send messages using PHP's mail() function.
984
     */
985
    public function isMail()
986
    {
987
        $this->Mailer = 'mail';
988
    }
989
990
    /**
991
     * Send messages using $Sendmail.
992
     */
993
    public function isSendmail()
994
    {
995
        $ini_sendmail_path = ini_get('sendmail_path');
996
997
        if (false === stripos($ini_sendmail_path, 'sendmail')) {
998
            $this->Sendmail = '/usr/sbin/sendmail';
999
        } else {
1000
            $this->Sendmail = $ini_sendmail_path;
1001
        }
1002
        $this->Mailer = 'sendmail';
1003
    }
1004
1005
    /**
1006
     * Send messages using qmail.
1007
     */
1008
    public function isQmail()
1009
    {
1010
        $ini_sendmail_path = ini_get('sendmail_path');
1011
1012
        if (false === stripos($ini_sendmail_path, 'qmail')) {
1013
            $this->Sendmail = '/var/qmail/bin/qmail-inject';
1014
        } else {
1015
            $this->Sendmail = $ini_sendmail_path;
1016
        }
1017
        $this->Mailer = 'qmail';
1018
    }
1019
1020
    /**
1021
     * Add a "To" address.
1022
     *
1023
     * @param string $address The email address to send to
1024
     * @param string $name
1025
     *
1026
     * @throws Exception
1027
     *
1028
     * @return bool true on success, false if address already used or invalid in some way
1029
     */
1030
    public function addAddress($address, $name = '')
1031
    {
1032
        return $this->addOrEnqueueAnAddress('to', $address, $name);
1033
    }
1034
1035
    /**
1036
     * Add a "CC" address.
1037
     *
1038
     * @param string $address The email address to send to
1039
     * @param string $name
1040
     *
1041
     * @throws Exception
1042
     *
1043
     * @return bool true on success, false if address already used or invalid in some way
1044
     */
1045
    public function addCC($address, $name = '')
1046
    {
1047
        return $this->addOrEnqueueAnAddress('cc', $address, $name);
1048
    }
1049
1050
    /**
1051
     * Add a "BCC" address.
1052
     *
1053
     * @param string $address The email address to send to
1054
     * @param string $name
1055
     *
1056
     * @throws Exception
1057
     *
1058
     * @return bool true on success, false if address already used or invalid in some way
1059
     */
1060
    public function addBCC($address, $name = '')
1061
    {
1062
        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
1063
    }
1064
1065
    /**
1066
     * Add a "Reply-To" address.
1067
     *
1068
     * @param string $address The email address to reply to
1069
     * @param string $name
1070
     *
1071
     * @throws Exception
1072
     *
1073
     * @return bool true on success, false if address already used or invalid in some way
1074
     */
1075
    public function addReplyTo($address, $name = '')
1076
    {
1077
        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
1078
    }
1079
1080
    /**
1081
     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
1082
     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
1083
     * be modified after calling this function), addition of such addresses is delayed until send().
1084
     * Addresses that have been added already return false, but do not throw exceptions.
1085
     *
1086
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'Reply-To'
1087
     * @param string $address The email address
1088
     * @param string $name    An optional username associated with the address
1089
     *
1090
     * @throws Exception
1091
     *
1092
     * @return bool true on success, false if address already used or invalid in some way
1093
     */
1094
    protected function addOrEnqueueAnAddress($kind, $address, $name)
1095
    {
1096
        $pos = false;
1097
        if ($address !== null) {
0 ignored issues
show
introduced by
The condition $address !== null is always true.
Loading history...
1098
            $address = trim($address);
1099
            $pos = strrpos($address, '@');
1100
        }
1101
        if (false === $pos) {
1102
            //At-sign is missing.
1103
            $error_message = sprintf(
1104
                '%s (%s): %s',
1105
                $this->lang('invalid_address'),
1106
                $kind,
1107
                $address
1108
            );
1109
            $this->setError($error_message);
1110
            $this->edebug($error_message);
1111
            if ($this->exceptions) {
1112
                throw new Exception($error_message);
1113
            }
1114
1115
            return false;
1116
        }
1117
        if ($name !== null && is_string($name)) {
0 ignored issues
show
introduced by
The condition is_string($name) is always true.
Loading history...
1118
            $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1119
        } else {
1120
            $name = '';
1121
        }
1122
        $params = [$kind, $address, $name];
1123
        //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
1124
        //Domain is assumed to be whatever is after the last @ symbol in the address
1125
        if ($this->has8bitChars(substr($address, ++$pos))) {
1126
            if (static::idnSupported()) {
1127
                if ('Reply-To' !== $kind) {
1128
                    if (!array_key_exists($address, $this->RecipientsQueue)) {
1129
                        $this->RecipientsQueue[$address] = $params;
1130
1131
                        return true;
1132
                    }
1133
                } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
1134
                    $this->ReplyToQueue[$address] = $params;
1135
1136
                    return true;
1137
                }
1138
            }
1139
            //We have an 8-bit domain, but we are missing the necessary extensions to support it
1140
            //Or we are already sending to this address
1141
            return false;
1142
        }
1143
1144
        //Immediately add standard addresses without IDN.
1145
        return call_user_func_array([$this, 'addAnAddress'], $params);
1146
    }
1147
1148
    /**
1149
     * Set the boundaries to use for delimiting MIME parts.
1150
     * If you override this, ensure you set all 3 boundaries to unique values.
1151
     * The default boundaries include a "=_" sequence which cannot occur in quoted-printable bodies,
1152
     * as suggested by https://www.rfc-editor.org/rfc/rfc2045#section-6.7
1153
     *
1154
     * @return void
1155
     */
1156
    public function setBoundaries()
1157
    {
1158
        $this->uniqueid = $this->generateId();
1159
        $this->boundary[1] = 'b1=_' . $this->uniqueid;
1160
        $this->boundary[2] = 'b2=_' . $this->uniqueid;
1161
        $this->boundary[3] = 'b3=_' . $this->uniqueid;
1162
    }
1163
1164
    /**
1165
     * Add an address to one of the recipient arrays or to the ReplyTo array.
1166
     * Addresses that have been added already return false, but do not throw exceptions.
1167
     *
1168
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1169
     * @param string $address The email address to send, resp. to reply to
1170
     * @param string $name
1171
     *
1172
     * @throws Exception
1173
     *
1174
     * @return bool true on success, false if address already used or invalid in some way
1175
     */
1176
    protected function addAnAddress($kind, $address, $name = '')
1177
    {
1178
        if (
1179
            self::$validator === 'php' &&
1180
            ((bool) preg_match('/[\x80-\xFF]/', $address))
1181
        ) {
1182
            //The caller has not altered the validator and is sending to an address
1183
            //with UTF-8, so assume that they want UTF-8 support instead of failing
1184
            $this->CharSet = self::CHARSET_UTF8;
1185
            self::$validator = 'eai';
1186
        }
1187
        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1188
            $error_message = sprintf(
1189
                '%s: %s',
1190
                $this->lang('Invalid recipient kind'),
1191
                $kind
1192
            );
1193
            $this->setError($error_message);
1194
            $this->edebug($error_message);
1195
            if ($this->exceptions) {
1196
                throw new Exception($error_message);
1197
            }
1198
1199
            return false;
1200
        }
1201
        if (!static::validateAddress($address)) {
1202
            $error_message = sprintf(
1203
                '%s (%s): %s',
1204
                $this->lang('invalid_address'),
1205
                $kind,
1206
                $address
1207
            );
1208
            $this->setError($error_message);
1209
            $this->edebug($error_message);
1210
            if ($this->exceptions) {
1211
                throw new Exception($error_message);
1212
            }
1213
1214
            return false;
1215
        }
1216
        if ('Reply-To' !== $kind) {
1217
            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
1218
                $this->{$kind}[] = [$address, $name];
1219
                $this->all_recipients[strtolower($address)] = true;
1220
1221
                return true;
1222
            }
1223
        } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
1224
            $this->ReplyTo[strtolower($address)] = [$address, $name];
1225
1226
            return true;
1227
        }
1228
1229
        return false;
1230
    }
1231
1232
    /**
1233
     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1234
     * of the form "display name <address>" into an array of name/address pairs.
1235
     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1236
     * Note that quotes in the name part are removed.
1237
     *
1238
     * @see https://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1239
     *
1240
     * @param string $addrstr The address list string
1241
     * @param bool   $useimap Whether to use the IMAP extension to parse the list
1242
     * @param string $charset The charset to use when decoding the address list string.
1243
     *
1244
     * @return array
1245
     */
1246
    public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591)
1247
    {
1248
        $addresses = [];
1249
        if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
1250
            //Use this built-in parser if it's available
1251
            $list = imap_rfc822_parse_adrlist($addrstr, '');
1252
            // Clear any potential IMAP errors to get rid of notices being thrown at end of script.
1253
            imap_errors();
1254
            foreach ($list as $address) {
1255
                if (
1256
                    '.SYNTAX-ERROR.' !== $address->host &&
1257
                    static::validateAddress($address->mailbox . '@' . $address->host)
1258
                ) {
1259
                    //Decode the name part if it's present and encoded
1260
                    if (
1261
                        property_exists($address, 'personal') &&
1262
                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
1263
                        defined('MB_CASE_UPPER') &&
1264
                        preg_match('/^=\?.*\?=$/s', $address->personal)
1265
                    ) {
1266
                        $origCharset = mb_internal_encoding();
1267
                        mb_internal_encoding($charset);
1268
                        //Undo any RFC2047-encoded spaces-as-underscores
1269
                        $address->personal = str_replace('_', '=20', $address->personal);
1270
                        //Decode the name
1271
                        $address->personal = mb_decode_mimeheader($address->personal);
1272
                        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

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

1528
                        /** @scrutinizer ignore-type */ $domain,
Loading history...
1529
                        \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI |
1530
                            \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII,
1531
                        \INTL_IDNA_VARIANT_UTS46
1532
                    );
1533
                } elseif (defined('INTL_IDNA_VARIANT_2003')) {
1534
                    //Fall back to this old, deprecated/removed encoding
1535
                    $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

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

1827
        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...
1828
            foreach ($this->SingleToArray as $toAddr) {
1829
                $mail = @popen($sendmail, 'w');
1830
                if (!$mail) {
1831
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1832
                }
1833
                $this->edebug("To: {$toAddr}");
1834
                fwrite($mail, 'To: ' . $toAddr . "\n");
1835
                fwrite($mail, $header);
1836
                fwrite($mail, $body);
1837
                $result = pclose($mail);
1838
                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
1839
                $this->doCallback(
1840
                    ($result === 0),
1841
                    [[$addrinfo['address'], $addrinfo['name']]],
1842
                    $this->cc,
1843
                    $this->bcc,
1844
                    $this->Subject,
1845
                    $body,
1846
                    $this->From,
1847
                    []
1848
                );
1849
                $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
1850
                if (0 !== $result) {
1851
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1852
                }
1853
            }
1854
        } else {
1855
            $mail = @popen($sendmail, 'w');
1856
            if (!$mail) {
0 ignored issues
show
introduced by
$mail is of type false|resource, thus it always evaluated to false.
Loading history...
1857
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1858
            }
1859
            fwrite($mail, $header);
1860
            fwrite($mail, $body);
1861
            $result = pclose($mail);
1862
            $this->doCallback(
1863
                ($result === 0),
1864
                $this->to,
1865
                $this->cc,
1866
                $this->bcc,
1867
                $this->Subject,
1868
                $body,
1869
                $this->From,
1870
                []
1871
            );
1872
            $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
1873
            if (0 !== $result) {
1874
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1875
            }
1876
        }
1877
1878
        return true;
1879
    }
1880
1881
    /**
1882
     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
1883
     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
1884
     *
1885
     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
1886
     *
1887
     * @param string $string The string to be validated
1888
     *
1889
     * @return bool
1890
     */
1891
    protected static function isShellSafe($string)
1892
    {
1893
        //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg,
1894
        //but some hosting providers disable it, creating a security problem that we don't want to have to deal with,
1895
        //so we don't.
1896
        if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) {
1897
            return false;
1898
        }
1899
1900
        if (
1901
            escapeshellcmd($string) !== $string
1902
            || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
1903
        ) {
1904
            return false;
1905
        }
1906
1907
        $length = strlen($string);
1908
1909
        for ($i = 0; $i < $length; ++$i) {
1910
            $c = $string[$i];
1911
1912
            //All other characters have a special meaning in at least one common shell, including = and +.
1913
            //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
1914
            //Note that this does permit non-Latin alphanumeric characters based on the current locale.
1915
            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
1916
                return false;
1917
            }
1918
        }
1919
1920
        return true;
1921
    }
1922
1923
    /**
1924
     * Check whether a file path is of a permitted type.
1925
     * Used to reject URLs and phar files from functions that access local file paths,
1926
     * such as addAttachment.
1927
     *
1928
     * @param string $path A relative or absolute path to a file
1929
     *
1930
     * @return bool
1931
     */
1932
    protected static function isPermittedPath($path)
1933
    {
1934
        //Matches scheme definition from https://www.rfc-editor.org/rfc/rfc3986#section-3.1
1935
        return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path);
1936
    }
1937
1938
    /**
1939
     * Check whether a file path is safe, accessible, and readable.
1940
     *
1941
     * @param string $path A relative or absolute path to a file
1942
     *
1943
     * @return bool
1944
     */
1945
    protected static function fileIsAccessible($path)
1946
    {
1947
        if (!static::isPermittedPath($path)) {
1948
            return false;
1949
        }
1950
        $readable = is_file($path);
1951
        //If not a UNC path (expected to start with \\), check read permission, see #2069
1952
        if (strpos($path, '\\\\') !== 0) {
1953
            $readable = $readable && is_readable($path);
1954
        }
1955
        return  $readable;
1956
    }
1957
1958
    /**
1959
     * Send mail using the PHP mail() function.
1960
     *
1961
     * @see https://www.php.net/manual/en/book.mail.php
1962
     *
1963
     * @param string $header The message headers
1964
     * @param string $body   The message body
1965
     *
1966
     * @throws Exception
1967
     *
1968
     * @return bool
1969
     */
1970
    protected function mailSend($header, $body)
1971
    {
1972
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1973
1974
        $toArr = [];
1975
        foreach ($this->to as $toaddr) {
1976
            $toArr[] = $this->addrFormat($toaddr);
1977
        }
1978
        $to = trim(implode(', ', $toArr));
1979
1980
        //If there are no To-addresses (e.g. when sending only to BCC-addresses)
1981
        //the following should be added to get a correct DKIM-signature.
1982
        //Compare with $this->preSend()
1983
        if ($to === '') {
1984
            $to = 'undisclosed-recipients:;';
1985
        }
1986
1987
        $params = null;
1988
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1989
        //A space after `-f` is optional, but there is a long history of its presence
1990
        //causing problems, so we don't use one
1991
        //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1992
        //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html
1993
        //Example problem: https://www.drupal.org/node/1057954
1994
        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1995
1996
        //PHP 5.6 workaround
1997
        $sendmail_from_value = ini_get('sendmail_from');
1998
        if (empty($this->Sender) && !empty($sendmail_from_value)) {
1999
            //PHP config has a sender address we can use
2000
            $this->Sender = ini_get('sendmail_from');
2001
        }
2002
        if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
2003
            if (self::isShellSafe($this->Sender)) {
2004
                $params = sprintf('-f%s', $this->Sender);
2005
            }
2006
            $old_from = ini_get('sendmail_from');
2007
            ini_set('sendmail_from', $this->Sender);
2008
        }
2009
        $result = false;
2010
        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

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

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

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

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

4571
                            $basedir . $directory . /** @scrutinizer ignore-type */ $filename,
Loading history...
4572
                            $cid,
4573
                            $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

4573
                            /** @scrutinizer ignore-type */ $filename,
Loading history...
4574
                            static::ENCODING_BASE64,
4575
                            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

4575
                            static::_mime_types((string) static::mb_pathinfo(/** @scrutinizer ignore-type */ $filename, PATHINFO_EXTENSION))
Loading history...
4576
                        )
4577
                    ) {
4578
                        $message = preg_replace(
4579
                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
4580
                            $images[1][$imgindex] . '="cid:' . $cid . '"',
4581
                            $message
4582
                        );
4583
                    }
4584
                }
4585
            }
4586
        }
4587
        $this->isHTML();
4588
        //Convert all message body line breaks to LE, makes quoted-printable encoding work much better
4589
        $this->Body = static::normalizeBreaks($message);
4590
        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
4591
        if (!$this->alternativeExists()) {
4592
            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
4593
                . static::$LE;
4594
        }
4595
4596
        return $this->Body;
4597
    }
4598
4599
    /**
4600
     * Convert an HTML string into plain text.
4601
     * This is used by msgHTML().
4602
     * Note - older versions of this function used a bundled advanced converter
4603
     * which was removed for license reasons in #232.
4604
     * Example usage:
4605
     *
4606
     * ```php
4607
     * //Use default conversion
4608
     * $plain = $mail->html2text($html);
4609
     * //Use your own custom converter
4610
     * $plain = $mail->html2text($html, function($html) {
4611
     *     $converter = new MyHtml2text($html);
4612
     *     return $converter->get_text();
4613
     * });
4614
     * ```
4615
     *
4616
     * @param string        $html     The HTML text to convert
4617
     * @param bool|callable $advanced Any boolean value to use the internal converter,
4618
     *                                or provide your own callable for custom conversion.
4619
     *                                *Never* pass user-supplied data into this parameter
4620
     *
4621
     * @return string
4622
     */
4623
    public function html2text($html, $advanced = false)
4624
    {
4625
        if (is_callable($advanced)) {
4626
            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

4626
            return call_user_func(/** @scrutinizer ignore-type */ $advanced, $html);
Loading history...
4627
        }
4628
4629
        return html_entity_decode(
4630
            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
4631
            ENT_QUOTES,
4632
            $this->CharSet
4633
        );
4634
    }
4635
4636
    /**
4637
     * Get the MIME type for a file extension.
4638
     *
4639
     * @param string $ext File extension
4640
     *
4641
     * @return string MIME type of file
4642
     */
4643
    public static function _mime_types($ext = '')
4644
    {
4645
        $mimes = [
4646
            'xl' => 'application/excel',
4647
            'js' => 'application/javascript',
4648
            'hqx' => 'application/mac-binhex40',
4649
            'cpt' => 'application/mac-compactpro',
4650
            'bin' => 'application/macbinary',
4651
            'doc' => 'application/msword',
4652
            'word' => 'application/msword',
4653
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
4654
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
4655
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
4656
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
4657
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
4658
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
4659
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
4660
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
4661
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
4662
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
4663
            'class' => 'application/octet-stream',
4664
            'dll' => 'application/octet-stream',
4665
            'dms' => 'application/octet-stream',
4666
            'exe' => 'application/octet-stream',
4667
            'lha' => 'application/octet-stream',
4668
            'lzh' => 'application/octet-stream',
4669
            'psd' => 'application/octet-stream',
4670
            'sea' => 'application/octet-stream',
4671
            'so' => 'application/octet-stream',
4672
            'oda' => 'application/oda',
4673
            'pdf' => 'application/pdf',
4674
            'ai' => 'application/postscript',
4675
            'eps' => 'application/postscript',
4676
            'ps' => 'application/postscript',
4677
            'smi' => 'application/smil',
4678
            'smil' => 'application/smil',
4679
            'mif' => 'application/vnd.mif',
4680
            'xls' => 'application/vnd.ms-excel',
4681
            'ppt' => 'application/vnd.ms-powerpoint',
4682
            'wbxml' => 'application/vnd.wap.wbxml',
4683
            'wmlc' => 'application/vnd.wap.wmlc',
4684
            'dcr' => 'application/x-director',
4685
            'dir' => 'application/x-director',
4686
            'dxr' => 'application/x-director',
4687
            'dvi' => 'application/x-dvi',
4688
            'gtar' => 'application/x-gtar',
4689
            'php3' => 'application/x-httpd-php',
4690
            'php4' => 'application/x-httpd-php',
4691
            'php' => 'application/x-httpd-php',
4692
            'phtml' => 'application/x-httpd-php',
4693
            'phps' => 'application/x-httpd-php-source',
4694
            'swf' => 'application/x-shockwave-flash',
4695
            'sit' => 'application/x-stuffit',
4696
            'tar' => 'application/x-tar',
4697
            'tgz' => 'application/x-tar',
4698
            'xht' => 'application/xhtml+xml',
4699
            'xhtml' => 'application/xhtml+xml',
4700
            'zip' => 'application/zip',
4701
            'mid' => 'audio/midi',
4702
            'midi' => 'audio/midi',
4703
            'mp2' => 'audio/mpeg',
4704
            'mp3' => 'audio/mpeg',
4705
            'm4a' => 'audio/mp4',
4706
            'mpga' => 'audio/mpeg',
4707
            'aif' => 'audio/x-aiff',
4708
            'aifc' => 'audio/x-aiff',
4709
            'aiff' => 'audio/x-aiff',
4710
            'ram' => 'audio/x-pn-realaudio',
4711
            'rm' => 'audio/x-pn-realaudio',
4712
            'rpm' => 'audio/x-pn-realaudio-plugin',
4713
            'ra' => 'audio/x-realaudio',
4714
            'wav' => 'audio/x-wav',
4715
            'mka' => 'audio/x-matroska',
4716
            'bmp' => 'image/bmp',
4717
            'gif' => 'image/gif',
4718
            'jpeg' => 'image/jpeg',
4719
            'jpe' => 'image/jpeg',
4720
            'jpg' => 'image/jpeg',
4721
            'png' => 'image/png',
4722
            'tiff' => 'image/tiff',
4723
            'tif' => 'image/tiff',
4724
            'webp' => 'image/webp',
4725
            'avif' => 'image/avif',
4726
            'heif' => 'image/heif',
4727
            'heifs' => 'image/heif-sequence',
4728
            'heic' => 'image/heic',
4729
            'heics' => 'image/heic-sequence',
4730
            'eml' => 'message/rfc822',
4731
            'css' => 'text/css',
4732
            'html' => 'text/html',
4733
            'htm' => 'text/html',
4734
            'shtml' => 'text/html',
4735
            'log' => 'text/plain',
4736
            'text' => 'text/plain',
4737
            'txt' => 'text/plain',
4738
            'rtx' => 'text/richtext',
4739
            'rtf' => 'text/rtf',
4740
            'vcf' => 'text/vcard',
4741
            'vcard' => 'text/vcard',
4742
            'ics' => 'text/calendar',
4743
            'xml' => 'text/xml',
4744
            'xsl' => 'text/xml',
4745
            'csv' => 'text/csv',
4746
            'wmv' => 'video/x-ms-wmv',
4747
            'mpeg' => 'video/mpeg',
4748
            'mpe' => 'video/mpeg',
4749
            'mpg' => 'video/mpeg',
4750
            'mp4' => 'video/mp4',
4751
            'm4v' => 'video/mp4',
4752
            'mov' => 'video/quicktime',
4753
            'qt' => 'video/quicktime',
4754
            'rv' => 'video/vnd.rn-realvideo',
4755
            'avi' => 'video/x-msvideo',
4756
            'movie' => 'video/x-sgi-movie',
4757
            'webm' => 'video/webm',
4758
            'mkv' => 'video/x-matroska',
4759
        ];
4760
        $ext = strtolower($ext);
4761
        if (array_key_exists($ext, $mimes)) {
4762
            return $mimes[$ext];
4763
        }
4764
4765
        return 'application/octet-stream';
4766
    }
4767
4768
    /**
4769
     * Map a file name to a MIME type.
4770
     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
4771
     *
4772
     * @param string $filename A file name or full path, does not need to exist as a file
4773
     *
4774
     * @return string
4775
     */
4776
    public static function filenameToType($filename)
4777
    {
4778
        //In case the path is a URL, strip any query string before getting extension
4779
        $qpos = strpos($filename, '?');
4780
        if (false !== $qpos) {
4781
            $filename = substr($filename, 0, $qpos);
4782
        }
4783
        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
4784
4785
        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

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