PHPMailer::preSend()   F
last analyzed

Complexity

Conditions 31
Paths 1632

Size

Total Lines 131

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 131
rs 0
c 0
b 0
f 0
cc 31
nc 1632
nop 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * PHPMailer - PHP email creation and transport class.
5
 * PHP Version 5.5.
6
 *
7
 * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
8
 *
9
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
10
 * @author    Jim Jagielski (jimjag) <[email protected]>
11
 * @author    Andy Prevost (codeworxtech) <[email protected]>
12
 * @author    Brent R. Matzelle (original founder)
13
 * @copyright 2012 - 2020 Marcus Bointon
14
 * @copyright 2010 - 2012 Jim Jagielski
15
 * @copyright 2004 - 2009 Andy Prevost
16
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
17
 * @note      This program is distributed in the hope that it will be useful - WITHOUT
18
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
 * FITNESS FOR A PARTICULAR PURPOSE.
20
 */
21
22
namespace PHPMailer\PHPMailer;
23
24
/**
25
 * PHPMailer - PHP email creation and transport class.
26
 *
27
 * @author Marcus Bointon (Synchro/coolbru) <[email protected]>
28
 * @author Jim Jagielski (jimjag) <[email protected]>
29
 * @author Andy Prevost (codeworxtech) <[email protected]>
30
 * @author Brent R. Matzelle (original founder)
31
 */
32
class PHPMailer
33
{
34
    const CHARSET_ASCII = 'us-ascii';
35
    const CHARSET_ISO88591 = 'iso-8859-1';
36
    const CHARSET_UTF8 = 'utf-8';
37
38
    const CONTENT_TYPE_PLAINTEXT = 'text/plain';
39
    const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';
40
    const CONTENT_TYPE_TEXT_HTML = 'text/html';
41
    const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
42
    const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
43
    const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
44
45
    const ENCODING_7BIT = '7bit';
46
    const ENCODING_8BIT = '8bit';
47
    const ENCODING_BASE64 = 'base64';
48
    const ENCODING_BINARY = 'binary';
49
    const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
50
51
    const ENCRYPTION_STARTTLS = 'tls';
52
    const ENCRYPTION_SMTPS = 'ssl';
53
54
    const ICAL_METHOD_REQUEST = 'REQUEST';
55
    const ICAL_METHOD_PUBLISH = 'PUBLISH';
56
    const ICAL_METHOD_REPLY = 'REPLY';
57
    const ICAL_METHOD_ADD = 'ADD';
58
    const ICAL_METHOD_CANCEL = 'CANCEL';
59
    const ICAL_METHOD_REFRESH = 'REFRESH';
60
    const ICAL_METHOD_COUNTER = 'COUNTER';
61
    const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
62
63
    /**
64
     * Email priority.
65
     * Options: null (default), 1 = High, 3 = Normal, 5 = low.
66
     * When null, the header is not set at all.
67
     *
68
     * @var int|null
69
     */
70
    public $Priority;
71
72
    /**
73
     * The character set of the message.
74
     *
75
     * @var string
76
     */
77
    public $CharSet = self::CHARSET_ISO88591;
78
79
    /**
80
     * The MIME Content-type of the message.
81
     *
82
     * @var string
83
     */
84
    public $ContentType = self::CONTENT_TYPE_PLAINTEXT;
85
86
    /**
87
     * The message encoding.
88
     * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
89
     *
90
     * @var string
91
     */
92
    public $Encoding = self::ENCODING_8BIT;
93
94
    /**
95
     * Holds the most recent mailer error message.
96
     *
97
     * @var string
98
     */
99
    public $ErrorInfo = '';
100
101
    /**
102
     * The From email address for the message.
103
     *
104
     * @var string
105
     */
106
    public $From = 'root@localhost';
107
108
    /**
109
     * The From name of the message.
110
     *
111
     * @var string
112
     */
113
    public $FromName = 'Root User';
114
115
    /**
116
     * The envelope sender of the message.
117
     * This will usually be turned into a Return-Path header by the receiver,
118
     * and is the address that bounces will be sent to.
119
     * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
120
     *
121
     * @var string
122
     */
123
    public $Sender = '';
124
125
    /**
126
     * The Subject of the message.
127
     *
128
     * @var string
129
     */
130
    public $Subject = '';
131
132
    /**
133
     * An HTML or plain text message body.
134
     * If HTML then call isHTML(true).
135
     *
136
     * @var string
137
     */
138
    public $Body = '';
139
140
    /**
141
     * The plain-text message body.
142
     * This body can be read by mail clients that do not have HTML email
143
     * capability such as mutt & Eudora.
144
     * Clients that can read HTML will view the normal Body.
145
     *
146
     * @var string
147
     */
148
    public $AltBody = '';
149
150
    /**
151
     * An iCal message part body.
152
     * Only supported in simple alt or alt_inline message types
153
     * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
154
     *
155
     * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/
156
     * @see http://kigkonsult.se/iCalcreator/
157
     *
158
     * @var string
159
     */
160
    public $Ical = '';
161
162
    /**
163
     * Value-array of "method" in Contenttype header "text/calendar"
164
     *
165
     * @var string[]
166
     */
167
    protected static $IcalMethods = [
168
        self::ICAL_METHOD_REQUEST,
169
        self::ICAL_METHOD_PUBLISH,
170
        self::ICAL_METHOD_REPLY,
171
        self::ICAL_METHOD_ADD,
172
        self::ICAL_METHOD_CANCEL,
173
        self::ICAL_METHOD_REFRESH,
174
        self::ICAL_METHOD_COUNTER,
175
        self::ICAL_METHOD_DECLINECOUNTER,
176
    ];
177
178
    /**
179
     * The complete compiled MIME message body.
180
     *
181
     * @var string
182
     */
183
    protected $MIMEBody = '';
184
185
    /**
186
     * The complete compiled MIME message headers.
187
     *
188
     * @var string
189
     */
190
    protected $MIMEHeader = '';
191
192
    /**
193
     * Extra headers that createHeader() doesn't fold in.
194
     *
195
     * @var string
196
     */
197
    protected $mailHeader = '';
198
199
    /**
200
     * Word-wrap the message body to this number of chars.
201
     * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
202
     *
203
     * @see static::STD_LINE_LENGTH
204
     *
205
     * @var int
206
     */
207
    public $WordWrap = 0;
208
209
    /**
210
     * Which method to use to send mail.
211
     * Options: "mail", "sendmail", or "smtp".
212
     *
213
     * @var string
214
     */
215
    public $Mailer = 'mail';
216
217
    /**
218
     * The path to the sendmail program.
219
     *
220
     * @var string
221
     */
222
    public $Sendmail = '/usr/sbin/sendmail';
223
224
    /**
225
     * Whether mail() uses a fully sendmail-compatible MTA.
226
     * One which supports sendmail's "-oi -f" options.
227
     *
228
     * @var bool
229
     */
230
    public $UseSendmailOptions = true;
231
232
    /**
233
     * The email address that a reading confirmation should be sent to, also known as read receipt.
234
     *
235
     * @var string
236
     */
237
    public $ConfirmReadingTo = '';
238
239
    /**
240
     * The hostname to use in the Message-ID header and as default HELO string.
241
     * If empty, PHPMailer attempts to find one with, in order,
242
     * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
243
     * 'localhost.localdomain'.
244
     *
245
     * @see PHPMailer::$Helo
246
     *
247
     * @var string
248
     */
249
    public $Hostname = '';
250
251
    /**
252
     * An ID to be used in the Message-ID header.
253
     * If empty, a unique id will be generated.
254
     * You can set your own, but it must be in the format "<id@domain>",
255
     * as defined in RFC5322 section 3.6.4 or it will be ignored.
256
     *
257
     * @see https://tools.ietf.org/html/rfc5322#section-3.6.4
258
     *
259
     * @var string
260
     */
261
    public $MessageID = '';
262
263
    /**
264
     * The message Date to be used in the Date header.
265
     * If empty, the current date will be added.
266
     *
267
     * @var string
268
     */
269
    public $MessageDate = '';
270
271
    /**
272
     * SMTP hosts.
273
     * Either a single hostname or multiple semicolon-delimited hostnames.
274
     * You can also specify a different port
275
     * for each host by using this format: [hostname:port]
276
     * (e.g. "smtp1.example.com:25;smtp2.example.com").
277
     * You can also specify encryption type, for example:
278
     * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
279
     * Hosts will be tried in order.
280
     *
281
     * @var string
282
     */
283
    public $Host = 'localhost';
284
285
    /**
286
     * The default SMTP server port.
287
     *
288
     * @var int
289
     */
290
    public $Port = 25;
291
292
    /**
293
     * The SMTP HELO/EHLO name used for the SMTP connection.
294
     * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
295
     * one with the same method described above for $Hostname.
296
     *
297
     * @see PHPMailer::$Hostname
298
     *
299
     * @var string
300
     */
301
    public $Helo = '';
302
303
    /**
304
     * What kind of encryption to use on the SMTP connection.
305
     * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
306
     *
307
     * @var string
308
     */
309
    public $SMTPSecure = '';
310
311
    /**
312
     * Whether to enable TLS encryption automatically if a server supports it,
313
     * even if `SMTPSecure` is not set to 'tls'.
314
     * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
315
     *
316
     * @var bool
317
     */
318
    public $SMTPAutoTLS = true;
319
320
    /**
321
     * Whether to use SMTP authentication.
322
     * Uses the Username and Password properties.
323
     *
324
     * @see PHPMailer::$Username
325
     * @see PHPMailer::$Password
326
     *
327
     * @var bool
328
     */
329
    public $SMTPAuth = false;
330
331
    /**
332
     * Options array passed to stream_context_create when connecting via SMTP.
333
     *
334
     * @var array
335
     */
336
    public $SMTPOptions = [];
337
338
    /**
339
     * SMTP username.
340
     *
341
     * @var string
342
     */
343
    public $Username = '';
344
345
    /**
346
     * SMTP password.
347
     *
348
     * @var string
349
     */
350
    public $Password = '';
351
352
    /**
353
     * SMTP auth type.
354
     * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified.
355
     *
356
     * @var string
357
     */
358
    public $AuthType = '';
359
360
    /**
361
     * An instance of the PHPMailer OAuth class.
362
     *
363
     * @var OAuth
364
     */
365
    protected $oauth;
366
367
    /**
368
     * The SMTP server timeout in seconds.
369
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
370
     *
371
     * @var int
372
     */
373
    public $Timeout = 300;
374
375
    /**
376
     * Comma separated list of DSN notifications
377
     * 'NEVER' under no circumstances a DSN must be returned to the sender.
378
     *         If you use NEVER all other notifications will be ignored.
379
     * 'SUCCESS' will notify you when your mail has arrived at its destination.
380
     * 'FAILURE' will arrive if an error occurred during delivery.
381
     * 'DELAY'   will notify you if there is an unusual delay in delivery, but the actual
382
     *           delivery's outcome (success or failure) is not yet decided.
383
     *
384
     * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
385
     */
386
    public $dsn = '';
387
388
    /**
389
     * SMTP class debug output mode.
390
     * Debug output level.
391
     * Options:
392
     * @see SMTP::DEBUG_OFF: No output
393
     * @see SMTP::DEBUG_CLIENT: Client messages
394
     * @see SMTP::DEBUG_SERVER: Client and server messages
395
     * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
396
     * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
397
     *
398
     * @see SMTP::$do_debug
399
     *
400
     * @var int
401
     */
402
    public $SMTPDebug = 0;
403
404
    /**
405
     * How to handle debug output.
406
     * Options:
407
     * * `echo` Output plain-text as-is, appropriate for CLI
408
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
409
     * * `error_log` Output to error log as configured in php.ini
410
     * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
411
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
412
     *
413
     * ```php
414
     * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
415
     * ```
416
     *
417
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
418
     * level output is used:
419
     *
420
     * ```php
421
     * $mail->Debugoutput = new myPsr3Logger;
422
     * ```
423
     *
424
     * @see SMTP::$Debugoutput
425
     *
426
     * @var string|callable|\Psr\Log\LoggerInterface
427
     */
428
    public $Debugoutput = 'echo';
429
430
    /**
431
     * Whether to keep SMTP connection open after each message.
432
     * If this is set to true then to close the connection
433
     * requires an explicit call to smtpClose().
434
     *
435
     * @var bool
436
     */
437
    public $SMTPKeepAlive = false;
438
439
    /**
440
     * Whether to split multiple to addresses into multiple messages
441
     * or send them all in one message.
442
     * Only supported in `mail` and `sendmail` transports, not in SMTP.
443
     *
444
     * @var bool
445
     *
446
     * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
447
     */
448
    public $SingleTo = false;
449
450
    /**
451
     * Storage for addresses when SingleTo is enabled.
452
     *
453
     * @var array
454
     */
455
    protected $SingleToArray = [];
456
457
    /**
458
     * Whether to generate VERP addresses on send.
459
     * Only applicable when sending via SMTP.
460
     *
461
     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
462
     * @see http://www.postfix.org/VERP_README.html Postfix VERP info
463
     *
464
     * @var bool
465
     */
466
    public $do_verp = false;
467
468
    /**
469
     * Whether to allow sending messages with an empty body.
470
     *
471
     * @var bool
472
     */
473
    public $AllowEmpty = false;
474
475
    /**
476
     * DKIM selector.
477
     *
478
     * @var string
479
     */
480
    public $DKIM_selector = '';
481
482
    /**
483
     * DKIM Identity.
484
     * Usually the email address used as the source of the email.
485
     *
486
     * @var string
487
     */
488
    public $DKIM_identity = '';
489
490
    /**
491
     * DKIM passphrase.
492
     * Used if your key is encrypted.
493
     *
494
     * @var string
495
     */
496
    public $DKIM_passphrase = '';
497
498
    /**
499
     * DKIM signing domain name.
500
     *
501
     * @example 'example.com'
502
     *
503
     * @var string
504
     */
505
    public $DKIM_domain = '';
506
507
    /**
508
     * DKIM Copy header field values for diagnostic use.
509
     *
510
     * @var bool
511
     */
512
    public $DKIM_copyHeaderFields = true;
513
514
    /**
515
     * DKIM Extra signing headers.
516
     *
517
     * @example ['List-Unsubscribe', 'List-Help']
518
     *
519
     * @var array
520
     */
521
    public $DKIM_extraHeaders = [];
522
523
    /**
524
     * DKIM private key file path.
525
     *
526
     * @var string
527
     */
528
    public $DKIM_private = '';
529
530
    /**
531
     * DKIM private key string.
532
     *
533
     * If set, takes precedence over `$DKIM_private`.
534
     *
535
     * @var string
536
     */
537
    public $DKIM_private_string = '';
538
539
    /**
540
     * Callback Action function name.
541
     *
542
     * The function that handles the result of the send email action.
543
     * It is called out by send() for each email sent.
544
     *
545
     * Value can be any php callable: http://www.php.net/is_callable
546
     *
547
     * Parameters:
548
     *   bool $result        result of the send action
549
     *   array   $to            email addresses of the recipients
550
     *   array   $cc            cc email addresses
551
     *   array   $bcc           bcc email addresses
552
     *   string  $subject       the subject
553
     *   string  $body          the email body
554
     *   string  $from          email address of sender
555
     *   string  $extra         extra information of possible use
556
     *                          "smtp_transaction_id' => last smtp transaction id
557
     *
558
     * @var string
559
     */
560
    public $action_function = '';
561
562
    /**
563
     * What to put in the X-Mailer header.
564
     * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
565
     *
566
     * @var string|null
567
     */
568
    public $XMailer = '';
569
570
    /**
571
     * Which validator to use by default when validating email addresses.
572
     * May be a callable to inject your own validator, but there are several built-in validators.
573
     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
574
     *
575
     * @see PHPMailer::validateAddress()
576
     *
577
     * @var string|callable
578
     */
579
    public static $validator = 'php';
580
581
    /**
582
     * An instance of the SMTP sender class.
583
     *
584
     * @var SMTP
585
     */
586
    protected $smtp;
587
588
    /**
589
     * The array of 'to' names and addresses.
590
     *
591
     * @var array
592
     */
593
    protected $to = [];
594
595
    /**
596
     * The array of 'cc' names and addresses.
597
     *
598
     * @var array
599
     */
600
    protected $cc = [];
601
602
    /**
603
     * The array of 'bcc' names and addresses.
604
     *
605
     * @var array
606
     */
607
    protected $bcc = [];
608
609
    /**
610
     * The array of reply-to names and addresses.
611
     *
612
     * @var array
613
     */
614
    protected $ReplyTo = [];
615
616
    /**
617
     * An array of all kinds of addresses.
618
     * Includes all of $to, $cc, $bcc.
619
     *
620
     * @see PHPMailer::$to
621
     * @see PHPMailer::$cc
622
     * @see PHPMailer::$bcc
623
     *
624
     * @var array
625
     */
626
    protected $all_recipients = [];
627
628
    /**
629
     * An array of names and addresses queued for validation.
630
     * In send(), valid and non duplicate entries are moved to $all_recipients
631
     * and one of $to, $cc, or $bcc.
632
     * This array is used only for addresses with IDN.
633
     *
634
     * @see PHPMailer::$to
635
     * @see PHPMailer::$cc
636
     * @see PHPMailer::$bcc
637
     * @see PHPMailer::$all_recipients
638
     *
639
     * @var array
640
     */
641
    protected $RecipientsQueue = [];
642
643
    /**
644
     * An array of reply-to names and addresses queued for validation.
645
     * In send(), valid and non duplicate entries are moved to $ReplyTo.
646
     * This array is used only for addresses with IDN.
647
     *
648
     * @see PHPMailer::$ReplyTo
649
     *
650
     * @var array
651
     */
652
    protected $ReplyToQueue = [];
653
654
    /**
655
     * The array of attachments.
656
     *
657
     * @var array
658
     */
659
    protected $attachment = [];
660
661
    /**
662
     * The array of custom headers.
663
     *
664
     * @var array
665
     */
666
    protected $CustomHeader = [];
667
668
    /**
669
     * The most recent Message-ID (including angular brackets).
670
     *
671
     * @var string
672
     */
673
    protected $lastMessageID = '';
674
675
    /**
676
     * The message's MIME type.
677
     *
678
     * @var string
679
     */
680
    protected $message_type = '';
681
682
    /**
683
     * The array of MIME boundary strings.
684
     *
685
     * @var array
686
     */
687
    protected $boundary = [];
688
689
    /**
690
     * The array of available languages.
691
     *
692
     * @var array
693
     */
694
    protected $language = [];
695
696
    /**
697
     * The number of errors encountered.
698
     *
699
     * @var int
700
     */
701
    protected $error_count = 0;
702
703
    /**
704
     * The S/MIME certificate file path.
705
     *
706
     * @var string
707
     */
708
    protected $sign_cert_file = '';
709
710
    /**
711
     * The S/MIME key file path.
712
     *
713
     * @var string
714
     */
715
    protected $sign_key_file = '';
716
717
    /**
718
     * The optional S/MIME extra certificates ("CA Chain") file path.
719
     *
720
     * @var string
721
     */
722
    protected $sign_extracerts_file = '';
723
724
    /**
725
     * The S/MIME password for the key.
726
     * Used only if the key is encrypted.
727
     *
728
     * @var string
729
     */
730
    protected $sign_key_pass = '';
731
732
    /**
733
     * Whether to throw exceptions for errors.
734
     *
735
     * @var bool
736
     */
737
    protected $exceptions = false;
738
739
    /**
740
     * Unique ID used for message ID and boundaries.
741
     *
742
     * @var string
743
     */
744
    protected $uniqueid = '';
745
746
    /**
747
     * The PHPMailer Version number.
748
     *
749
     * @var string
750
     */
751
    const VERSION = '6.2.0';
752
753
    /**
754
     * Error severity: message only, continue processing.
755
     *
756
     * @var int
757
     */
758
    const STOP_MESSAGE = 0;
759
760
    /**
761
     * Error severity: message, likely ok to continue processing.
762
     *
763
     * @var int
764
     */
765
    const STOP_CONTINUE = 1;
766
767
    /**
768
     * Error severity: message, plus full stop, critical error reached.
769
     *
770
     * @var int
771
     */
772
    const STOP_CRITICAL = 2;
773
774
    /**
775
     * The SMTP standard CRLF line break.
776
     * If you want to change line break format, change static::$LE, not this.
777
     */
778
    const CRLF = "\r\n";
779
780
    /**
781
     * "Folding White Space" a white space string used for line folding.
782
     */
783
    const FWS = ' ';
784
785
    /**
786
     * SMTP RFC standard line ending; Carriage Return, Line Feed.
787
     *
788
     * @var string
789
     */
790
    protected static $LE = self::CRLF;
791
792
    /**
793
     * The maximum line length supported by mail().
794
     *
795
     * Background: mail() will sometimes corrupt messages
796
     * with headers headers longer than 65 chars, see #818.
797
     *
798
     * @var int
799
     */
800
    const MAIL_MAX_LINE_LENGTH = 63;
801
802
    /**
803
     * The maximum line length allowed by RFC 2822 section 2.1.1.
804
     *
805
     * @var int
806
     */
807
    const MAX_LINE_LENGTH = 998;
808
809
    /**
810
     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
811
     * This length does NOT include the line break
812
     * 76 means that lines will be 77 or 78 chars depending on whether
813
     * the line break format is LF or CRLF; both are valid.
814
     *
815
     * @var int
816
     */
817
    const STD_LINE_LENGTH = 76;
818
819
    /**
820
     * Constructor.
821
     *
822
     * @param bool $exceptions Should we throw external exceptions?
823
     */
824
    public function __construct($exceptions = null)
825
    {
826
        if (null !== $exceptions) {
827
            $this->exceptions = (bool) $exceptions;
828
        }
829
        //Pick an appropriate debug output format automatically
830
        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
831
    }
832
833
    /**
834
     * Destructor.
835
     */
836
    public function __destruct()
837
    {
838
        //Close any open SMTP connection nicely
839
        $this->smtpClose();
840
    }
841
842
    /**
843
     * Call mail() in a safe_mode-aware fashion.
844
     * Also, unless sendmail_path points to sendmail (or something that
845
     * claims to be sendmail), don't pass params (not a perfect fix,
846
     * but it will do).
847
     *
848
     * @param string      $to      To
849
     * @param string      $subject Subject
850
     * @param string      $body    Message Body
851
     * @param string      $header  Additional Header(s)
852
     * @param string|null $params  Params
853
     *
854
     * @return bool
855
     */
856
    private function mailPassthru($to, $subject, $body, $header, $params)
857
    {
858
        //Check overloading of mail function to avoid double-encoding
859
        if (ini_get('mbstring.func_overload') & 1) {
860
            $subject = $this->secureHeader($subject);
861
        } else {
862
            $subject = $this->encodeHeader($this->secureHeader($subject));
863
        }
864
        //Calling mail() with null params breaks
865
        if (!$this->UseSendmailOptions || null === $params) {
866
            $result = @mail($to, $subject, $body, $header);
867
        } else {
868
            $result = @mail($to, $subject, $body, $header, $params);
869
        }
870
871
        return $result;
872
    }
873
874
    /**
875
     * Output debugging info via user-defined method.
876
     * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug).
877
     *
878
     * @see PHPMailer::$Debugoutput
879
     * @see PHPMailer::$SMTPDebug
880
     *
881
     * @param string $str
882
     */
883
    protected function edebug($str)
884
    {
885
        if ($this->SMTPDebug <= 0) {
886
            return;
887
        }
888
        //Is this a PSR-3 logger?
889
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
0 ignored issues
show
Bug introduced by
The class Psr\Log\LoggerInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
890
            $this->Debugoutput->debug($str);
891
892
            return;
893
        }
894
        //Avoid clash with built-in function names
895
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
896
            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
897
898
            return;
899
        }
900
        switch ($this->Debugoutput) {
901
            case 'error_log':
902
                //Don't output, just log
903
                /** @noinspection ForgottenDebugOutputInspection */
904
                error_log($str);
905
                break;
906
            case 'html':
907
                //Cleans up output a bit for a better looking, HTML-safe output
908
                echo htmlentities(
909
                    preg_replace('/[\r\n]+/', '', $str),
910
                    ENT_QUOTES,
911
                    'UTF-8'
912
                ), "<br>\n";
913
                break;
914
            case 'echo':
915
            default:
916
                //Normalize line breaks
917
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
918
                echo gmdate('Y-m-d H:i:s'),
919
                "\t",
920
                    //Trim trailing space
921
                trim(
922
                    //Indent for readability, except for trailing break
923
                    str_replace(
924
                        "\n",
925
                        "\n                   \t                  ",
926
                        trim($str)
927
                    )
928
                ),
929
                "\n";
930
        }
931
    }
932
933
    /**
934
     * Sets message type to HTML or plain.
935
     *
936
     * @param bool $isHtml True for HTML mode
937
     */
938
    public function isHTML($isHtml = true)
939
    {
940
        if ($isHtml) {
941
            $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
942
        } else {
943
            $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
944
        }
945
    }
946
947
    /**
948
     * Send messages using SMTP.
949
     */
950
    public function isSMTP()
951
    {
952
        $this->Mailer = 'smtp';
953
    }
954
955
    /**
956
     * Send messages using PHP's mail() function.
957
     */
958
    public function isMail()
959
    {
960
        $this->Mailer = 'mail';
961
    }
962
963
    /**
964
     * Send messages using $Sendmail.
965
     */
966
    public function isSendmail()
967
    {
968
        $ini_sendmail_path = ini_get('sendmail_path');
969
970
        if (false === stripos($ini_sendmail_path, 'sendmail')) {
971
            $this->Sendmail = '/usr/sbin/sendmail';
972
        } else {
973
            $this->Sendmail = $ini_sendmail_path;
974
        }
975
        $this->Mailer = 'sendmail';
976
    }
977
978
    /**
979
     * Send messages using qmail.
980
     */
981
    public function isQmail()
982
    {
983
        $ini_sendmail_path = ini_get('sendmail_path');
984
985
        if (false === stripos($ini_sendmail_path, 'qmail')) {
986
            $this->Sendmail = '/var/qmail/bin/qmail-inject';
987
        } else {
988
            $this->Sendmail = $ini_sendmail_path;
989
        }
990
        $this->Mailer = 'qmail';
991
    }
992
993
    /**
994
     * Add a "To" address.
995
     *
996
     * @param string $address The email address to send to
997
     * @param string $name
998
     *
999
     * @throws Exception
1000
     *
1001
     * @return bool true on success, false if address already used or invalid in some way
1002
     */
1003
    public function addAddress($address, $name = '')
1004
    {
1005
        return $this->addOrEnqueueAnAddress('to', $address, $name);
1006
    }
1007
1008
    /**
1009
     * Add a "CC" address.
1010
     *
1011
     * @param string $address The email address to send to
1012
     * @param string $name
1013
     *
1014
     * @throws Exception
1015
     *
1016
     * @return bool true on success, false if address already used or invalid in some way
1017
     */
1018
    public function addCC($address, $name = '')
1019
    {
1020
        return $this->addOrEnqueueAnAddress('cc', $address, $name);
1021
    }
1022
1023
    /**
1024
     * Add a "BCC" address.
1025
     *
1026
     * @param string $address The email address to send to
1027
     * @param string $name
1028
     *
1029
     * @throws Exception
1030
     *
1031
     * @return bool true on success, false if address already used or invalid in some way
1032
     */
1033
    public function addBCC($address, $name = '')
1034
    {
1035
        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
1036
    }
1037
1038
    /**
1039
     * Add a "Reply-To" address.
1040
     *
1041
     * @param string $address The email address to reply to
1042
     * @param string $name
1043
     *
1044
     * @throws Exception
1045
     *
1046
     * @return bool true on success, false if address already used or invalid in some way
1047
     */
1048
    public function addReplyTo($address, $name = '')
1049
    {
1050
        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
1051
    }
1052
1053
    /**
1054
     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
1055
     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
1056
     * be modified after calling this function), addition of such addresses is delayed until send().
1057
     * Addresses that have been added already return false, but do not throw exceptions.
1058
     *
1059
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1060
     * @param string $address The email address to send, resp. to reply to
1061
     * @param string $name
1062
     *
1063
     * @throws Exception
1064
     *
1065
     * @return bool true on success, false if address already used or invalid in some way
1066
     */
1067
    protected function addOrEnqueueAnAddress($kind, $address, $name)
1068
    {
1069
        $address = trim($address);
1070
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1071
        $pos = strrpos($address, '@');
1072
        if (false === $pos) {
1073
            // At-sign is missing.
1074
            $error_message = sprintf(
1075
                '%s (%s): %s',
1076
                $this->lang('invalid_address'),
1077
                $kind,
1078
                $address
1079
            );
1080
            $this->setError($error_message);
1081
            $this->edebug($error_message);
1082
            if ($this->exceptions) {
1083
                throw new Exception($error_message);
1084
            }
1085
1086
            return false;
1087
        }
1088
        $params = [$kind, $address, $name];
1089
        // Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
1090
        if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
1091
            if ('Reply-To' !== $kind) {
1092
                if (!array_key_exists($address, $this->RecipientsQueue)) {
1093
                    $this->RecipientsQueue[$address] = $params;
1094
1095
                    return true;
1096
                }
1097
            } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
1098
                $this->ReplyToQueue[$address] = $params;
1099
1100
                return true;
1101
            }
1102
1103
            return false;
1104
        }
1105
1106
        // Immediately add standard addresses without IDN.
1107
        return call_user_func_array([$this, 'addAnAddress'], $params);
1108
    }
1109
1110
    /**
1111
     * Add an address to one of the recipient arrays or to the ReplyTo array.
1112
     * Addresses that have been added already return false, but do not throw exceptions.
1113
     *
1114
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1115
     * @param string $address The email address to send, resp. to reply to
1116
     * @param string $name
1117
     *
1118
     * @throws Exception
1119
     *
1120
     * @return bool true on success, false if address already used or invalid in some way
1121
     */
1122
    protected function addAnAddress($kind, $address, $name = '')
1123
    {
1124
        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1125
            $error_message = sprintf(
1126
                '%s: %s',
1127
                $this->lang('Invalid recipient kind'),
1128
                $kind
1129
            );
1130
            $this->setError($error_message);
1131
            $this->edebug($error_message);
1132
            if ($this->exceptions) {
1133
                throw new Exception($error_message);
1134
            }
1135
1136
            return false;
1137
        }
1138
        if (!static::validateAddress($address)) {
1139
            $error_message = sprintf(
1140
                '%s (%s): %s',
1141
                $this->lang('invalid_address'),
1142
                $kind,
1143
                $address
1144
            );
1145
            $this->setError($error_message);
1146
            $this->edebug($error_message);
1147
            if ($this->exceptions) {
1148
                throw new Exception($error_message);
1149
            }
1150
1151
            return false;
1152
        }
1153
        if ('Reply-To' !== $kind) {
1154
            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
1155
                $this->{$kind}[] = [$address, $name];
1156
                $this->all_recipients[strtolower($address)] = true;
1157
1158
                return true;
1159
            }
1160
        } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
1161
            $this->ReplyTo[strtolower($address)] = [$address, $name];
1162
1163
            return true;
1164
        }
1165
1166
        return false;
1167
    }
1168
1169
    /**
1170
     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1171
     * of the form "display name <address>" into an array of name/address pairs.
1172
     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1173
     * Note that quotes in the name part are removed.
1174
     *
1175
     * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1176
     *
1177
     * @param string $addrstr The address list string
1178
     * @param bool   $useimap Whether to use the IMAP extension to parse the list
1179
     *
1180
     * @return array
1181
     */
1182
    public static function parseAddresses($addrstr, $useimap = true)
1183
    {
1184
        $addresses = [];
1185
        if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
1186
            //Use this built-in parser if it's available
1187
            $list = imap_rfc822_parse_adrlist($addrstr, '');
1188
            foreach ($list as $address) {
1189
                if (
1190
                    ('.SYNTAX-ERROR.' !== $address->host) && static::validateAddress(
1191
                        $address->mailbox . '@' . $address->host
1192
                    )
1193
                ) {
1194
                    $addresses[] = [
1195
                        'name' => (property_exists($address, 'personal') ? $address->personal : ''),
1196
                        'address' => $address->mailbox . '@' . $address->host,
1197
                    ];
1198
                }
1199
            }
1200
        } else {
1201
            //Use this simpler parser
1202
            $list = explode(',', $addrstr);
1203
            foreach ($list as $address) {
1204
                $address = trim($address);
1205
                //Is there a separate name part?
1206
                if (strpos($address, '<') === false) {
1207
                    //No separate name, just use the whole thing
1208
                    if (static::validateAddress($address)) {
1209
                        $addresses[] = [
1210
                            'name' => '',
1211
                            'address' => $address,
1212
                        ];
1213
                    }
1214
                } else {
1215
                    list($name, $email) = explode('<', $address);
1216
                    $email = trim(str_replace('>', '', $email));
1217
                    if (static::validateAddress($email)) {
1218
                        $addresses[] = [
1219
                            'name' => trim(str_replace(['"', "'"], '', $name)),
1220
                            'address' => $email,
1221
                        ];
1222
                    }
1223
                }
1224
            }
1225
        }
1226
1227
        return $addresses;
1228
    }
1229
1230
    /**
1231
     * Set the From and FromName properties.
1232
     *
1233
     * @param string $address
1234
     * @param string $name
1235
     * @param bool   $auto    Whether to also set the Sender address, defaults to true
1236
     *
1237
     * @throws Exception
1238
     *
1239
     * @return bool
1240
     */
1241
    public function setFrom($address, $name = '', $auto = true)
1242
    {
1243
        $address = trim($address);
1244
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1245
        // Don't validate now addresses with IDN. Will be done in send().
1246
        $pos = strrpos($address, '@');
1247
        if (
1248
            (false === $pos)
1249
            || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
1250
            && !static::validateAddress($address))
1251
        ) {
1252
            $error_message = sprintf(
1253
                '%s (From): %s',
1254
                $this->lang('invalid_address'),
1255
                $address
1256
            );
1257
            $this->setError($error_message);
1258
            $this->edebug($error_message);
1259
            if ($this->exceptions) {
1260
                throw new Exception($error_message);
1261
            }
1262
1263
            return false;
1264
        }
1265
        $this->From = $address;
1266
        $this->FromName = $name;
1267
        if ($auto && empty($this->Sender)) {
1268
            $this->Sender = $address;
1269
        }
1270
1271
        return true;
1272
    }
1273
1274
    /**
1275
     * Return the Message-ID header of the last email.
1276
     * Technically this is the value from the last time the headers were created,
1277
     * but it's also the message ID of the last sent message except in
1278
     * pathological cases.
1279
     *
1280
     * @return string
1281
     */
1282
    public function getLastMessageID()
1283
    {
1284
        return $this->lastMessageID;
1285
    }
1286
1287
    /**
1288
     * Check that a string looks like an email address.
1289
     * Validation patterns supported:
1290
     * * `auto` Pick best pattern automatically;
1291
     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
1292
     * * `pcre` Use old PCRE implementation;
1293
     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
1294
     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
1295
     * * `noregex` Don't use a regex: super fast, really dumb.
1296
     * Alternatively you may pass in a callable to inject your own validator, for example:
1297
     *
1298
     * ```php
1299
     * PHPMailer::validateAddress('[email protected]', function($address) {
1300
     *     return (strpos($address, '@') !== false);
1301
     * });
1302
     * ```
1303
     *
1304
     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
1305
     *
1306
     * @param string          $address       The email address to check
1307
     * @param string|callable $patternselect Which pattern to use
1308
     *
1309
     * @return bool
1310
     */
1311
    public static function validateAddress($address, $patternselect = null)
1312
    {
1313
        if (null === $patternselect) {
1314
            $patternselect = static::$validator;
1315
        }
1316
        if (is_callable($patternselect)) {
1317
            return call_user_func($patternselect, $address);
1318
        }
1319
        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
1320
        if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
1321
            return false;
1322
        }
1323
        switch ($patternselect) {
1324
            case 'pcre': //Kept for BC
1325
            case 'pcre8':
1326
                /*
1327
                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
1328
                 * is based.
1329
                 * In addition to the addresses allowed by filter_var, also permits:
1330
                 *  * dotless domains: `a@b`
1331
                 *  * comments: `1234 @ local(blah) .machine .example`
1332
                 *  * quoted elements: `'"test blah"@example.org'`
1333
                 *  * numeric TLDs: `[email protected]`
1334
                 *  * unbracketed IPv4 literals: `[email protected]`
1335
                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'
1336
                 * Not all of these will necessarily work for sending!
1337
                 *
1338
                 * @see       http://squiloople.com/2009/12/20/email-address-validation/
1339
                 * @copyright 2009-2010 Michael Rushton
1340
                 * Feel free to use and redistribute this code. But please keep this copyright notice.
1341
                 */
1342
                return (bool) preg_match(
1343
                    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
1344
                    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
1345
                    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
1346
                    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
1347
                    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
1348
                    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
1349
                    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
1350
                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
1351
                    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
1352
                    $address
1353
                );
1354
            case 'html5':
1355
                /*
1356
                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
1357
                 *
1358
                 * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
1359
                 */
1360
                return (bool) preg_match(
1361
                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
1362
                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
1363
                    $address
1364
                );
1365
            case 'php':
1366
            default:
1367
                return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
1368
        }
1369
    }
1370
1371
    /**
1372
     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
1373
     * `intl` and `mbstring` PHP extensions.
1374
     *
1375
     * @return bool `true` if required functions for IDN support are present
1376
     */
1377
    public static function idnSupported()
1378
    {
1379
        return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
1380
    }
1381
1382
    /**
1383
     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
1384
     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
1385
     * This function silently returns unmodified address if:
1386
     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
1387
     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
1388
     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).
1389
     *
1390
     * @see PHPMailer::$CharSet
1391
     *
1392
     * @param string $address The email address to convert
1393
     *
1394
     * @return string The encoded address in ASCII form
1395
     */
1396
    public function punyencodeAddress($address)
1397
    {
1398
        // Verify we have required functions, CharSet, and at-sign.
1399
        $pos = strrpos($address, '@');
1400
        if (
1401
            !empty($this->CharSet) &&
1402
            false !== $pos &&
1403
            static::idnSupported()
1404
        ) {
1405
            $domain = substr($address, ++$pos);
1406
            // Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
1407
            if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
1408
                $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet);
1409
                //Ignore IDE complaints about this line - method signature changed in PHP 5.4
1410
                $errorcode = 0;
1411
                if (defined('INTL_IDNA_VARIANT_UTS46')) {
1412
                    $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46);
1413
                } elseif (defined('INTL_IDNA_VARIANT_2003')) {
1414
                    $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_2003);
1415
                } else {
1416
                    $punycode = idn_to_ascii($domain, $errorcode);
1417
                }
1418
                if (false !== $punycode) {
1419
                    return substr($address, 0, $pos) . $punycode;
1420
                }
1421
            }
1422
        }
1423
1424
        return $address;
1425
    }
1426
1427
    /**
1428
     * Create a message and send it.
1429
     * Uses the sending method specified by $Mailer.
1430
     *
1431
     * @throws Exception
1432
     *
1433
     * @return bool false on error - See the ErrorInfo property for details of the error
1434
     */
1435
    public function send()
1436
    {
1437
        try {
1438
            if (!$this->preSend()) {
1439
                return false;
1440
            }
1441
1442
            return $this->postSend();
1443
        } catch (Exception $exc) {
1444
            $this->mailHeader = '';
1445
            $this->setError($exc->getMessage());
1446
            if ($this->exceptions) {
1447
                throw $exc;
1448
            }
1449
1450
            return false;
1451
        }
1452
    }
1453
1454
    /**
1455
     * Prepare a message for sending.
1456
     *
1457
     * @throws Exception
1458
     *
1459
     * @return bool
1460
     */
1461
    public function preSend()
1462
    {
1463
        if (
1464
            'smtp' === $this->Mailer
1465
            || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
1466
        ) {
1467
            //SMTP mandates RFC-compliant line endings
1468
            //and it's also used with mail() on Windows
1469
            static::setLE(self::CRLF);
1470
        } else {
1471
            //Maintain backward compatibility with legacy Linux command line mailers
1472
            static::setLE(PHP_EOL);
1473
        }
1474
        //Check for buggy PHP versions that add a header with an incorrect line break
1475
        if (
1476
            'mail' === $this->Mailer
1477
            && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
1478
                || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
1479
            && ini_get('mail.add_x_header') === '1'
1480
            && stripos(PHP_OS, 'WIN') === 0
1481
        ) {
1482
            trigger_error(
1483
                'Your version of PHP is affected by a bug that may result in corrupted messages.' .
1484
                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
1485
                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
1486
                E_USER_WARNING
1487
            );
1488
        }
1489
1490
        try {
1491
            $this->error_count = 0; // Reset errors
1492
            $this->mailHeader = '';
1493
1494
            // Dequeue recipient and Reply-To addresses with IDN
1495
            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
1496
                $params[1] = $this->punyencodeAddress($params[1]);
1497
                call_user_func_array([$this, 'addAnAddress'], $params);
1498
            }
1499
            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
1500
                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
1501
            }
1502
1503
            // Validate From, Sender, and ConfirmReadingTo addresses
1504
            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
1505
                $this->$address_kind = trim($this->$address_kind);
1506
                if (empty($this->$address_kind)) {
1507
                    continue;
1508
                }
1509
                $this->$address_kind = $this->punyencodeAddress($this->$address_kind);
1510
                if (!static::validateAddress($this->$address_kind)) {
1511
                    $error_message = sprintf(
1512
                        '%s (%s): %s',
1513
                        $this->lang('invalid_address'),
1514
                        $address_kind,
1515
                        $this->$address_kind
1516
                    );
1517
                    $this->setError($error_message);
1518
                    $this->edebug($error_message);
1519
                    if ($this->exceptions) {
1520
                        throw new Exception($error_message);
1521
                    }
1522
1523
                    return false;
1524
                }
1525
            }
1526
1527
            // Set whether the message is multipart/alternative
1528
            if ($this->alternativeExists()) {
1529
                $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
1530
            }
1531
1532
            $this->setMessageType();
1533
            // Refuse to send an empty message unless we are specifically allowing it
1534
            if (!$this->AllowEmpty && empty($this->Body)) {
1535
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
1536
            }
1537
1538
            //Trim subject consistently
1539
            $this->Subject = trim($this->Subject);
1540
            // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
1541
            $this->MIMEHeader = '';
1542
            $this->MIMEBody = $this->createBody();
1543
            // createBody may have added some headers, so retain them
1544
            $tempheaders = $this->MIMEHeader;
1545
            $this->MIMEHeader = $this->createHeader();
1546
            $this->MIMEHeader .= $tempheaders;
1547
1548
            // To capture the complete message when using mail(), create
1549
            // an extra header list which createHeader() doesn't fold in
1550
            if ('mail' === $this->Mailer) {
1551
                if (count($this->to) > 0) {
1552
                    $this->mailHeader .= $this->addrAppend('To', $this->to);
1553
                } else {
1554
                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
1555
                }
1556
                $this->mailHeader .= $this->headerLine(
1557
                    'Subject',
1558
                    $this->encodeHeader($this->secureHeader($this->Subject))
1559
                );
1560
            }
1561
1562
            // Sign with DKIM if enabled
1563
            if (
1564
                !empty($this->DKIM_domain)
1565
                && !empty($this->DKIM_selector)
1566
                && (!empty($this->DKIM_private_string)
1567
                    || (!empty($this->DKIM_private)
1568
                        && static::isPermittedPath($this->DKIM_private)
1569
                        && file_exists($this->DKIM_private)
1570
                    )
1571
                )
1572
            ) {
1573
                $header_dkim = $this->DKIM_Add(
1574
                    $this->MIMEHeader . $this->mailHeader,
1575
                    $this->encodeHeader($this->secureHeader($this->Subject)),
1576
                    $this->MIMEBody
1577
                );
1578
                $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .
1579
                    static::normalizeBreaks($header_dkim) . static::$LE;
1580
            }
1581
1582
            return true;
1583
        } catch (Exception $exc) {
1584
            $this->setError($exc->getMessage());
1585
            if ($this->exceptions) {
1586
                throw $exc;
1587
            }
1588
1589
            return false;
1590
        }
1591
    }
1592
1593
    /**
1594
     * Actually send a message via the selected mechanism.
1595
     *
1596
     * @throws Exception
1597
     *
1598
     * @return bool
1599
     */
1600
    public function postSend()
1601
    {
1602
        try {
1603
            // Choose the mailer and send through it
1604
            switch ($this->Mailer) {
1605
                case 'sendmail':
1606
                case 'qmail':
1607
                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
1608
                case 'smtp':
1609
                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
1610
                case 'mail':
1611
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1612
                default:
1613
                    $sendMethod = $this->Mailer . 'Send';
1614
                    if (method_exists($this, $sendMethod)) {
1615
                        return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody);
1616
                    }
1617
1618
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1619
            }
1620
        } catch (Exception $exc) {
1621
            if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

Loading history...
1622
                $this->smtp->reset();
1623
            }
1624
            $this->setError($exc->getMessage());
1625
            $this->edebug($exc->getMessage());
1626
            if ($this->exceptions) {
1627
                throw $exc;
1628
            }
1629
        }
1630
1631
        return false;
1632
    }
1633
1634
    /**
1635
     * Send mail using the $Sendmail program.
1636
     *
1637
     * @see PHPMailer::$Sendmail
1638
     *
1639
     * @param string $header The message headers
1640
     * @param string $body   The message body
1641
     *
1642
     * @throws Exception
1643
     *
1644
     * @return bool
1645
     */
1646
    protected function sendmailSend($header, $body)
1647
    {
1648
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1649
1650
        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1651
        if (!empty($this->Sender) && self::isShellSafe($this->Sender)) {
1652
            if ('qmail' === $this->Mailer) {
1653
                $sendmailFmt = '%s -f%s';
1654
            } else {
1655
                $sendmailFmt = '%s -oi -f%s -t';
1656
            }
1657
        } elseif ('qmail' === $this->Mailer) {
1658
            $sendmailFmt = '%s';
1659
        } else {
1660
            $sendmailFmt = '%s -oi -t';
1661
        }
1662
1663
        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
1664
1665
        if ($this->SingleTo) {
0 ignored issues
show
Deprecated Code introduced by
The property PHPMailer\PHPMailer\PHPMailer::$SingleTo has been deprecated with message: 6.0.0 PHPMailer isn't a mailing list manager!

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...
1666
            foreach ($this->SingleToArray as $toAddr) {
1667
                $mail = @popen($sendmail, 'w');
1668
                if (!$mail) {
1669
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1670
                }
1671
                fwrite($mail, 'To: ' . $toAddr . "\n");
1672
                fwrite($mail, $header);
1673
                fwrite($mail, $body);
1674
                $result = pclose($mail);
1675
                $this->doCallback(
1676
                    ($result === 0),
1677
                    [$toAddr],
1678
                    $this->cc,
1679
                    $this->bcc,
1680
                    $this->Subject,
1681
                    $body,
1682
                    $this->From,
1683
                    []
1684
                );
1685
                if (0 !== $result) {
1686
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1687
                }
1688
            }
1689
        } else {
1690
            $mail = @popen($sendmail, 'w');
1691
            if (!$mail) {
1692
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1693
            }
1694
            fwrite($mail, $header);
1695
            fwrite($mail, $body);
1696
            $result = pclose($mail);
1697
            $this->doCallback(
1698
                ($result === 0),
1699
                $this->to,
1700
                $this->cc,
1701
                $this->bcc,
1702
                $this->Subject,
1703
                $body,
1704
                $this->From,
1705
                []
1706
            );
1707
            if (0 !== $result) {
1708
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1709
            }
1710
        }
1711
1712
        return true;
1713
    }
1714
1715
    /**
1716
     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
1717
     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
1718
     *
1719
     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
1720
     *
1721
     * @param string $string The string to be validated
1722
     *
1723
     * @return bool
1724
     */
1725
    protected static function isShellSafe($string)
1726
    {
1727
        // Future-proof
1728
        if (
1729
            escapeshellcmd($string) !== $string
1730
            || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
1731
        ) {
1732
            return false;
1733
        }
1734
1735
        $length = strlen($string);
1736
1737
        for ($i = 0; $i < $length; ++$i) {
1738
            $c = $string[$i];
1739
1740
            // All other characters have a special meaning in at least one common shell, including = and +.
1741
            // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
1742
            // Note that this does permit non-Latin alphanumeric characters based on the current locale.
1743
            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
1744
                return false;
1745
            }
1746
        }
1747
1748
        return true;
1749
    }
1750
1751
    /**
1752
     * Check whether a file path is of a permitted type.
1753
     * Used to reject URLs and phar files from functions that access local file paths,
1754
     * such as addAttachment.
1755
     *
1756
     * @param string $path A relative or absolute path to a file
1757
     *
1758
     * @return bool
1759
     */
1760
    protected static function isPermittedPath($path)
1761
    {
1762
        return !preg_match('#^[a-z]+://#i', $path);
1763
    }
1764
1765
    /**
1766
     * Check whether a file path is safe, accessible, and readable.
1767
     *
1768
     * @param string $path A relative or absolute path to a file
1769
     *
1770
     * @return bool
1771
     */
1772
    protected static function fileIsAccessible($path)
1773
    {
1774
        $readable = file_exists($path);
1775
        //If not a UNC path (expected to start with \\), check read permission, see #2069
1776
        if (strpos($path, '\\\\') !== 0) {
1777
            $readable = $readable && is_readable($path);
1778
        }
1779
        return static::isPermittedPath($path) && $readable;
1780
    }
1781
1782
    /**
1783
     * Send mail using the PHP mail() function.
1784
     *
1785
     * @see http://www.php.net/manual/en/book.mail.php
1786
     *
1787
     * @param string $header The message headers
1788
     * @param string $body   The message body
1789
     *
1790
     * @throws Exception
1791
     *
1792
     * @return bool
1793
     */
1794
    protected function mailSend($header, $body)
1795
    {
1796
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1797
1798
        $toArr = [];
1799
        foreach ($this->to as $toaddr) {
1800
            $toArr[] = $this->addrFormat($toaddr);
1801
        }
1802
        $to = implode(', ', $toArr);
1803
1804
        $params = null;
1805
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1806
        //A space after `-f` is optional, but there is a long history of its presence
1807
        //causing problems, so we don't use one
1808
        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1809
        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
1810
        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
1811
        //Example problem: https://www.drupal.org/node/1057954
1812
        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1813
        if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
1814
            $params = sprintf('-f%s', $this->Sender);
1815
        }
1816
        if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
1817
            $old_from = ini_get('sendmail_from');
1818
            ini_set('sendmail_from', $this->Sender);
1819
        }
1820
        $result = false;
1821
        if ($this->SingleTo && count($toArr) > 1) {
0 ignored issues
show
Deprecated Code introduced by
The property PHPMailer\PHPMailer\PHPMailer::$SingleTo has been deprecated with message: 6.0.0 PHPMailer isn't a mailing list manager!

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

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...
2425
                foreach ($this->to as $toaddr) {
2426
                    $this->SingleToArray[] = $this->addrFormat($toaddr);
2427
                }
2428
            } elseif (count($this->to) > 0) {
2429
                $result .= $this->addrAppend('To', $this->to);
2430
            } elseif (count($this->cc) === 0) {
2431
                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
2432
            }
2433
        }
2434
        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
2435
2436
        // sendmail and mail() extract Cc from the header before sending
2437
        if (count($this->cc) > 0) {
2438
            $result .= $this->addrAppend('Cc', $this->cc);
2439
        }
2440
2441
        // sendmail and mail() extract Bcc from the header before sending
2442
        if (
2443
            (
2444
                'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
2445
            )
2446
            && count($this->bcc) > 0
2447
        ) {
2448
            $result .= $this->addrAppend('Bcc', $this->bcc);
2449
        }
2450
2451
        if (count($this->ReplyTo) > 0) {
2452
            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
2453
        }
2454
2455
        // mail() sets the subject itself
2456
        if ('mail' !== $this->Mailer) {
2457
            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
2458
        }
2459
2460
        // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
2461
        // https://tools.ietf.org/html/rfc5322#section-3.6.4
2462
        if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageID)) {
2463
            $this->lastMessageID = $this->MessageID;
2464
        } else {
2465
            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
2466
        }
2467
        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
2468
        if (null !== $this->Priority) {
2469
            $result .= $this->headerLine('X-Priority', $this->Priority);
2470
        }
2471
        if ('' === $this->XMailer) {
2472
            $result .= $this->headerLine(
2473
                'X-Mailer',
2474
                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
2475
            );
2476
        } else {
2477
            $myXmailer = trim($this->XMailer);
2478
            if ($myXmailer) {
2479
                $result .= $this->headerLine('X-Mailer', $myXmailer);
2480
            }
2481
        }
2482
2483
        if ('' !== $this->ConfirmReadingTo) {
2484
            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
2485
        }
2486
2487
        // Add custom headers
2488
        foreach ($this->CustomHeader as $header) {
2489
            $result .= $this->headerLine(
2490
                trim($header[0]),
2491
                $this->encodeHeader(trim($header[1]))
2492
            );
2493
        }
2494
        if (!$this->sign_key_file) {
2495
            $result .= $this->headerLine('MIME-Version', '1.0');
2496
            $result .= $this->getMailMIME();
2497
        }
2498
2499
        return $result;
2500
    }
2501
2502
    /**
2503
     * Get the message MIME type headers.
2504
     *
2505
     * @return string
2506
     */
2507
    public function getMailMIME()
2508
    {
2509
        $result = '';
2510
        $ismultipart = true;
2511
        switch ($this->message_type) {
2512
            case 'inline':
2513
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2514
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2515
                break;
2516
            case 'attach':
2517
            case 'inline_attach':
2518
            case 'alt_attach':
2519
            case 'alt_inline_attach':
2520
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';');
2521
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2522
                break;
2523
            case 'alt':
2524
            case 'alt_inline':
2525
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2526
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2527
                break;
2528
            default:
2529
                // Catches case 'plain': and case '':
2530
                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
2531
                $ismultipart = false;
2532
                break;
2533
        }
2534
        // RFC1341 part 5 says 7bit is assumed if not specified
2535
        if (static::ENCODING_7BIT !== $this->Encoding) {
2536
            // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
2537
            if ($ismultipart) {
2538
                if (static::ENCODING_8BIT === $this->Encoding) {
2539
                    $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
2540
                }
2541
                // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
2542
            } else {
2543
                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
2544
            }
2545
        }
2546
2547
        if ('mail' !== $this->Mailer) {
2548
//            $result .= static::$LE;
2549
        }
2550
2551
        return $result;
2552
    }
2553
2554
    /**
2555
     * Returns the whole MIME message.
2556
     * Includes complete headers and body.
2557
     * Only valid post preSend().
2558
     *
2559
     * @see PHPMailer::preSend()
2560
     *
2561
     * @return string
2562
     */
2563
    public function getSentMIMEMessage()
2564
    {
2565
        return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) .
2566
            static::$LE . static::$LE . $this->MIMEBody;
2567
    }
2568
2569
    /**
2570
     * Create a unique ID to use for boundaries.
2571
     *
2572
     * @return string
2573
     */
2574
    protected function generateId()
2575
    {
2576
        $len = 32; //32 bytes = 256 bits
2577
        $bytes = '';
2578
        if (function_exists('random_bytes')) {
2579
            try {
2580
                $bytes = random_bytes($len);
2581
            } catch (\Exception $e) {
2582
                //Do nothing
2583
            }
2584
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
2585
            /** @noinspection CryptographicallySecureRandomnessInspection */
2586
            $bytes = openssl_random_pseudo_bytes($len);
2587
        }
2588
        if ($bytes === '') {
2589
            //We failed to produce a proper random string, so make do.
2590
            //Use a hash to force the length to the same as the other methods
2591
            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
2592
        }
2593
2594
        //We don't care about messing up base64 format here, just want a random string
2595
        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
2596
    }
2597
2598
    /**
2599
     * Assemble the message body.
2600
     * Returns an empty string on failure.
2601
     *
2602
     * @throws Exception
2603
     *
2604
     * @return string The assembled message body
2605
     */
2606
    public function createBody()
2607
    {
2608
        $body = '';
2609
        //Create unique IDs and preset boundaries
2610
        $this->uniqueid = $this->generateId();
2611
        $this->boundary[1] = 'b1_' . $this->uniqueid;
2612
        $this->boundary[2] = 'b2_' . $this->uniqueid;
2613
        $this->boundary[3] = 'b3_' . $this->uniqueid;
2614
2615
        if ($this->sign_key_file) {
2616
            $body .= $this->getMailMIME() . static::$LE;
2617
        }
2618
2619
        $this->setWordWrap();
2620
2621
        $bodyEncoding = $this->Encoding;
2622
        $bodyCharSet = $this->CharSet;
2623
        //Can we do a 7-bit downgrade?
2624
        if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {
2625
            $bodyEncoding = static::ENCODING_7BIT;
2626
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2627
            $bodyCharSet = static::CHARSET_ASCII;
2628
        }
2629
        //If lines are too long, and we're not already using an encoding that will shorten them,
2630
        //change to quoted-printable transfer encoding for the body part only
2631
        if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {
2632
            $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2633
        }
2634
2635
        $altBodyEncoding = $this->Encoding;
2636
        $altBodyCharSet = $this->CharSet;
2637
        //Can we do a 7-bit downgrade?
2638
        if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {
2639
            $altBodyEncoding = static::ENCODING_7BIT;
2640
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2641
            $altBodyCharSet = static::CHARSET_ASCII;
2642
        }
2643
        //If lines are too long, and we're not already using an encoding that will shorten them,
2644
        //change to quoted-printable transfer encoding for the alt body part only
2645
        if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {
2646
            $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2647
        }
2648
        //Use this as a preamble in all multipart message types
2649
        $mimepre = 'This is a multi-part message in MIME format.' . static::$LE . static::$LE;
2650
        switch ($this->message_type) {
2651
            case 'inline':
2652
                $body .= $mimepre;
2653
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2654
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2655
                $body .= static::$LE;
2656
                $body .= $this->attachAll('inline', $this->boundary[1]);
2657
                break;
2658
            case 'attach':
2659
                $body .= $mimepre;
2660
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2661
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2662
                $body .= static::$LE;
2663
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2664
                break;
2665
            case 'inline_attach':
2666
                $body .= $mimepre;
2667
                $body .= $this->textLine('--' . $this->boundary[1]);
2668
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2669
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
2670
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2671
                $body .= static::$LE;
2672
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
2673
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2674
                $body .= static::$LE;
2675
                $body .= $this->attachAll('inline', $this->boundary[2]);
2676
                $body .= static::$LE;
2677
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2678
                break;
2679
            case 'alt':
2680
                $body .= $mimepre;
2681
                $body .= $this->getBoundary(
2682
                    $this->boundary[1],
2683
                    $altBodyCharSet,
2684
                    static::CONTENT_TYPE_PLAINTEXT,
2685
                    $altBodyEncoding
2686
                );
2687
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2688
                $body .= static::$LE;
2689
                $body .= $this->getBoundary(
2690
                    $this->boundary[1],
2691
                    $bodyCharSet,
2692
                    static::CONTENT_TYPE_TEXT_HTML,
2693
                    $bodyEncoding
2694
                );
2695
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2696
                $body .= static::$LE;
2697
                if (!empty($this->Ical)) {
2698
                    $method = static::ICAL_METHOD_REQUEST;
2699
                    foreach (static::$IcalMethods as $imethod) {
2700
                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
2701
                            $method = $imethod;
2702
                            break;
2703
                        }
2704
                    }
2705
                    $body .= $this->getBoundary(
2706
                        $this->boundary[1],
2707
                        '',
2708
                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
2709
                        ''
2710
                    );
2711
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2712
                    $body .= static::$LE;
2713
                }
2714
                $body .= $this->endBoundary($this->boundary[1]);
2715
                break;
2716
            case 'alt_inline':
2717
                $body .= $mimepre;
2718
                $body .= $this->getBoundary(
2719
                    $this->boundary[1],
2720
                    $altBodyCharSet,
2721
                    static::CONTENT_TYPE_PLAINTEXT,
2722
                    $altBodyEncoding
2723
                );
2724
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2725
                $body .= static::$LE;
2726
                $body .= $this->textLine('--' . $this->boundary[1]);
2727
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2728
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
2729
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2730
                $body .= static::$LE;
2731
                $body .= $this->getBoundary(
2732
                    $this->boundary[2],
2733
                    $bodyCharSet,
2734
                    static::CONTENT_TYPE_TEXT_HTML,
2735
                    $bodyEncoding
2736
                );
2737
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2738
                $body .= static::$LE;
2739
                $body .= $this->attachAll('inline', $this->boundary[2]);
2740
                $body .= static::$LE;
2741
                $body .= $this->endBoundary($this->boundary[1]);
2742
                break;
2743
            case 'alt_attach':
2744
                $body .= $mimepre;
2745
                $body .= $this->textLine('--' . $this->boundary[1]);
2746
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2747
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
2748
                $body .= static::$LE;
2749
                $body .= $this->getBoundary(
2750
                    $this->boundary[2],
2751
                    $altBodyCharSet,
2752
                    static::CONTENT_TYPE_PLAINTEXT,
2753
                    $altBodyEncoding
2754
                );
2755
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2756
                $body .= static::$LE;
2757
                $body .= $this->getBoundary(
2758
                    $this->boundary[2],
2759
                    $bodyCharSet,
2760
                    static::CONTENT_TYPE_TEXT_HTML,
2761
                    $bodyEncoding
2762
                );
2763
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2764
                $body .= static::$LE;
2765
                if (!empty($this->Ical)) {
2766
                    $method = static::ICAL_METHOD_REQUEST;
2767
                    foreach (static::$IcalMethods as $imethod) {
2768
                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
2769
                            $method = $imethod;
2770
                            break;
2771
                        }
2772
                    }
2773
                    $body .= $this->getBoundary(
2774
                        $this->boundary[2],
2775
                        '',
2776
                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
2777
                        ''
2778
                    );
2779
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2780
                }
2781
                $body .= $this->endBoundary($this->boundary[2]);
2782
                $body .= static::$LE;
2783
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2784
                break;
2785
            case 'alt_inline_attach':
2786
                $body .= $mimepre;
2787
                $body .= $this->textLine('--' . $this->boundary[1]);
2788
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2789
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
2790
                $body .= static::$LE;
2791
                $body .= $this->getBoundary(
2792
                    $this->boundary[2],
2793
                    $altBodyCharSet,
2794
                    static::CONTENT_TYPE_PLAINTEXT,
2795
                    $altBodyEncoding
2796
                );
2797
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2798
                $body .= static::$LE;
2799
                $body .= $this->textLine('--' . $this->boundary[2]);
2800
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2801
                $body .= $this->textLine(' boundary="' . $this->boundary[3] . '";');
2802
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2803
                $body .= static::$LE;
2804
                $body .= $this->getBoundary(
2805
                    $this->boundary[3],
2806
                    $bodyCharSet,
2807
                    static::CONTENT_TYPE_TEXT_HTML,
2808
                    $bodyEncoding
2809
                );
2810
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2811
                $body .= static::$LE;
2812
                $body .= $this->attachAll('inline', $this->boundary[3]);
2813
                $body .= static::$LE;
2814
                $body .= $this->endBoundary($this->boundary[2]);
2815
                $body .= static::$LE;
2816
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2817
                break;
2818
            default:
2819
                // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
2820
                //Reset the `Encoding` property in case we changed it for line length reasons
2821
                $this->Encoding = $bodyEncoding;
2822
                $body .= $this->encodeString($this->Body, $this->Encoding);
2823
                break;
2824
        }
2825
2826
        if ($this->isError()) {
2827
            $body = '';
2828
            if ($this->exceptions) {
2829
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
2830
            }
2831
        } elseif ($this->sign_key_file) {
2832
            try {
2833
                if (!defined('PKCS7_TEXT')) {
2834
                    throw new Exception($this->lang('extension_missing') . 'openssl');
2835
                }
2836
2837
                $file = tempnam(sys_get_temp_dir(), 'srcsign');
2838
                $signed = tempnam(sys_get_temp_dir(), 'mailsign');
2839
                file_put_contents($file, $body);
2840
2841
                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
2842
                if (empty($this->sign_extracerts_file)) {
2843
                    $sign = @openssl_pkcs7_sign(
2844
                        $file,
2845
                        $signed,
2846
                        'file://' . realpath($this->sign_cert_file),
2847
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2848
                        []
2849
                    );
2850
                } else {
2851
                    $sign = @openssl_pkcs7_sign(
2852
                        $file,
2853
                        $signed,
2854
                        'file://' . realpath($this->sign_cert_file),
2855
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2856
                        [],
2857
                        PKCS7_DETACHED,
2858
                        $this->sign_extracerts_file
2859
                    );
2860
                }
2861
2862
                @unlink($file);
2863
                if ($sign) {
2864
                    $body = file_get_contents($signed);
2865
                    @unlink($signed);
2866
                    //The message returned by openssl contains both headers and body, so need to split them up
2867
                    $parts = explode("\n\n", $body, 2);
2868
                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
2869
                    $body = $parts[1];
2870
                } else {
2871
                    @unlink($signed);
2872
                    throw new Exception($this->lang('signing') . openssl_error_string());
2873
                }
2874
            } catch (Exception $exc) {
2875
                $body = '';
2876
                if ($this->exceptions) {
2877
                    throw $exc;
2878
                }
2879
            }
2880
        }
2881
2882
        return $body;
2883
    }
2884
2885
    /**
2886
     * Return the start of a message boundary.
2887
     *
2888
     * @param string $boundary
2889
     * @param string $charSet
2890
     * @param string $contentType
2891
     * @param string $encoding
2892
     *
2893
     * @return string
2894
     */
2895
    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
2896
    {
2897
        $result = '';
2898
        if ('' === $charSet) {
2899
            $charSet = $this->CharSet;
2900
        }
2901
        if ('' === $contentType) {
2902
            $contentType = $this->ContentType;
2903
        }
2904
        if ('' === $encoding) {
2905
            $encoding = $this->Encoding;
2906
        }
2907
        $result .= $this->textLine('--' . $boundary);
2908
        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
2909
        $result .= static::$LE;
2910
        // RFC1341 part 5 says 7bit is assumed if not specified
2911
        if (static::ENCODING_7BIT !== $encoding) {
2912
            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
2913
        }
2914
        $result .= static::$LE;
2915
2916
        return $result;
2917
    }
2918
2919
    /**
2920
     * Return the end of a message boundary.
2921
     *
2922
     * @param string $boundary
2923
     *
2924
     * @return string
2925
     */
2926
    protected function endBoundary($boundary)
2927
    {
2928
        return static::$LE . '--' . $boundary . '--' . static::$LE;
2929
    }
2930
2931
    /**
2932
     * Set the message type.
2933
     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
2934
     */
2935
    protected function setMessageType()
2936
    {
2937
        $type = [];
2938
        if ($this->alternativeExists()) {
2939
            $type[] = 'alt';
2940
        }
2941
        if ($this->inlineImageExists()) {
2942
            $type[] = 'inline';
2943
        }
2944
        if ($this->attachmentExists()) {
2945
            $type[] = 'attach';
2946
        }
2947
        $this->message_type = implode('_', $type);
2948
        if ('' === $this->message_type) {
2949
            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
2950
            $this->message_type = 'plain';
2951
        }
2952
    }
2953
2954
    /**
2955
     * Format a header line.
2956
     *
2957
     * @param string     $name
2958
     * @param string|int $value
2959
     *
2960
     * @return string
2961
     */
2962
    public function headerLine($name, $value)
2963
    {
2964
        return $name . ': ' . $value . static::$LE;
2965
    }
2966
2967
    /**
2968
     * Return a formatted mail line.
2969
     *
2970
     * @param string $value
2971
     *
2972
     * @return string
2973
     */
2974
    public function textLine($value)
2975
    {
2976
        return $value . static::$LE;
2977
    }
2978
2979
    /**
2980
     * Add an attachment from a path on the filesystem.
2981
     * Never use a user-supplied path to a file!
2982
     * Returns false if the file could not be found or read.
2983
     * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.
2984
     * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
2985
     *
2986
     * @param string $path        Path to the attachment
2987
     * @param string $name        Overrides the attachment name
2988
     * @param string $encoding    File encoding (see $Encoding)
2989
     * @param string $type        MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
2990
     * @param string $disposition Disposition to use
2991
     *
2992
     * @throws Exception
2993
     *
2994
     * @return bool
2995
     */
2996
    public function addAttachment(
2997
        $path,
2998
        $name = '',
2999
        $encoding = self::ENCODING_BASE64,
3000
        $type = '',
3001
        $disposition = 'attachment'
3002
    ) {
3003
        try {
3004
            if (!static::fileIsAccessible($path)) {
3005
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
3006
            }
3007
3008
            // If a MIME type is not specified, try to work it out from the file name
3009
            if ('' === $type) {
3010
                $type = static::filenameToType($path);
3011
            }
3012
3013
            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
3014
            if ('' === $name) {
3015
                $name = $filename;
3016
            }
3017
            if (!$this->validateEncoding($encoding)) {
3018
                throw new Exception($this->lang('encoding') . $encoding);
3019
            }
3020
3021
            $this->attachment[] = [
3022
                0 => $path,
3023
                1 => $filename,
3024
                2 => $name,
3025
                3 => $encoding,
3026
                4 => $type,
3027
                5 => false, // isStringAttachment
3028
                6 => $disposition,
3029
                7 => $name,
3030
            ];
3031
        } catch (Exception $exc) {
3032
            $this->setError($exc->getMessage());
3033
            $this->edebug($exc->getMessage());
3034
            if ($this->exceptions) {
3035
                throw $exc;
3036
            }
3037
3038
            return false;
3039
        }
3040
3041
        return true;
3042
    }
3043
3044
    /**
3045
     * Return the array of attachments.
3046
     *
3047
     * @return array
3048
     */
3049
    public function getAttachments()
3050
    {
3051
        return $this->attachment;
3052
    }
3053
3054
    /**
3055
     * Attach all file, string, and binary attachments to the message.
3056
     * Returns an empty string on failure.
3057
     *
3058
     * @param string $disposition_type
3059
     * @param string $boundary
3060
     *
3061
     * @throws Exception
3062
     *
3063
     * @return string
3064
     */
3065
    protected function attachAll($disposition_type, $boundary)
3066
    {
3067
        // Return text of body
3068
        $mime = [];
3069
        $cidUniq = [];
3070
        $incl = [];
3071
3072
        // Add all attachments
3073
        foreach ($this->attachment as $attachment) {
3074
            // Check if it is a valid disposition_filter
3075
            if ($attachment[6] === $disposition_type) {
3076
                // Check for string attachment
3077
                $string = '';
3078
                $path = '';
3079
                $bString = $attachment[5];
3080
                if ($bString) {
3081
                    $string = $attachment[0];
3082
                } else {
3083
                    $path = $attachment[0];
3084
                }
3085
3086
                $inclhash = hash('sha256', serialize($attachment));
3087
                if (in_array($inclhash, $incl, true)) {
3088
                    continue;
3089
                }
3090
                $incl[] = $inclhash;
3091
                $name = $attachment[2];
3092
                $encoding = $attachment[3];
3093
                $type = $attachment[4];
3094
                $disposition = $attachment[6];
3095
                $cid = $attachment[7];
3096
                if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {
3097
                    continue;
3098
                }
3099
                $cidUniq[$cid] = true;
3100
3101
                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
3102
                //Only include a filename property if we have one
3103
                if (!empty($name)) {
3104
                    $mime[] = sprintf(
3105
                        'Content-Type: %s; name=%s%s',
3106
                        $type,
3107
                        static::quotedString($this->encodeHeader($this->secureHeader($name))),
3108
                        static::$LE
3109
                    );
3110
                } else {
3111
                    $mime[] = sprintf(
3112
                        'Content-Type: %s%s',
3113
                        $type,
3114
                        static::$LE
3115
                    );
3116
                }
3117
                // RFC1341 part 5 says 7bit is assumed if not specified
3118
                if (static::ENCODING_7BIT !== $encoding) {
3119
                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
3120
                }
3121
3122
                //Only set Content-IDs on inline attachments
3123
                if ((string) $cid !== '' && $disposition === 'inline') {
3124
                    $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
3125
                }
3126
3127
                // Allow for bypassing the Content-Disposition header
3128
                if (!empty($disposition)) {
3129
                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
3130
                    if (!empty($encoded_name)) {
3131
                        $mime[] = sprintf(
3132
                            'Content-Disposition: %s; filename=%s%s',
3133
                            $disposition,
3134
                            static::quotedString($encoded_name),
3135
                            static::$LE . static::$LE
3136
                        );
3137
                    } else {
3138
                        $mime[] = sprintf(
3139
                            'Content-Disposition: %s%s',
3140
                            $disposition,
3141
                            static::$LE . static::$LE
3142
                        );
3143
                    }
3144
                } else {
3145
                    $mime[] = static::$LE;
3146
                }
3147
3148
                // Encode as string attachment
3149
                if ($bString) {
3150
                    $mime[] = $this->encodeString($string, $encoding);
3151
                } else {
3152
                    $mime[] = $this->encodeFile($path, $encoding);
3153
                }
3154
                if ($this->isError()) {
3155
                    return '';
3156
                }
3157
                $mime[] = static::$LE;
3158
            }
3159
        }
3160
3161
        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
3162
3163
        return implode('', $mime);
3164
    }
3165
3166
    /**
3167
     * Encode a file attachment in requested format.
3168
     * Returns an empty string on failure.
3169
     *
3170
     * @param string $path     The full path to the file
3171
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3172
     *
3173
     * @return string
3174
     */
3175
    protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
3176
    {
3177
        try {
3178
            if (!static::fileIsAccessible($path)) {
3179
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
3180
            }
3181
            $file_buffer = file_get_contents($path);
3182
            if (false === $file_buffer) {
3183
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
3184
            }
3185
            $file_buffer = $this->encodeString($file_buffer, $encoding);
3186
3187
            return $file_buffer;
3188
        } catch (Exception $exc) {
3189
            $this->setError($exc->getMessage());
3190
            $this->edebug($exc->getMessage());
3191
            if ($this->exceptions) {
3192
                throw $exc;
3193
            }
3194
3195
            return '';
3196
        }
3197
    }
3198
3199
    /**
3200
     * Encode a string in requested format.
3201
     * Returns an empty string on failure.
3202
     *
3203
     * @param string $str      The text to encode
3204
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3205
     *
3206
     * @throws Exception
3207
     *
3208
     * @return string
3209
     */
3210
    public function encodeString($str, $encoding = self::ENCODING_BASE64)
3211
    {
3212
        $encoded = '';
3213
        switch (strtolower($encoding)) {
3214
            case static::ENCODING_BASE64:
3215
                $encoded = chunk_split(
3216
                    base64_encode($str),
3217
                    static::STD_LINE_LENGTH,
3218
                    static::$LE
3219
                );
3220
                break;
3221
            case static::ENCODING_7BIT:
3222
            case static::ENCODING_8BIT:
3223
                $encoded = static::normalizeBreaks($str);
3224
                // Make sure it ends with a line break
3225
                if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
3226
                    $encoded .= static::$LE;
3227
                }
3228
                break;
3229
            case static::ENCODING_BINARY:
3230
                $encoded = $str;
3231
                break;
3232
            case static::ENCODING_QUOTED_PRINTABLE:
3233
                $encoded = $this->encodeQP($str);
3234
                break;
3235
            default:
3236
                $this->setError($this->lang('encoding') . $encoding);
3237
                if ($this->exceptions) {
3238
                    throw new Exception($this->lang('encoding') . $encoding);
3239
                }
3240
                break;
3241
        }
3242
3243
        return $encoded;
3244
    }
3245
3246
    /**
3247
     * Encode a header value (not including its label) optimally.
3248
     * Picks shortest of Q, B, or none. Result includes folding if needed.
3249
     * See RFC822 definitions for phrase, comment and text positions.
3250
     *
3251
     * @param string $str      The header value to encode
3252
     * @param string $position What context the string will be used in
3253
     *
3254
     * @return string
3255
     */
3256
    public function encodeHeader($str, $position = 'text')
3257
    {
3258
        $matchcount = 0;
3259
        switch (strtolower($position)) {
3260
            case 'phrase':
3261
                if (!preg_match('/[\200-\377]/', $str)) {
3262
                    // Can't use addslashes as we don't know the value of magic_quotes_sybase
3263
                    $encoded = addcslashes($str, "\0..\37\177\\\"");
3264
                    if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
3265
                        return $encoded;
3266
                    }
3267
3268
                    return "\"$encoded\"";
3269
                }
3270
                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
3271
                break;
3272
            /* @noinspection PhpMissingBreakStatementInspection */
3273
            case 'comment':
3274
                $matchcount = preg_match_all('/[()"]/', $str, $matches);
3275
            //fallthrough
3276
            case 'text':
3277
            default:
3278
                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
3279
                break;
3280
        }
3281
3282
        if ($this->has8bitChars($str)) {
3283
            $charset = $this->CharSet;
3284
        } else {
3285
            $charset = static::CHARSET_ASCII;
3286
        }
3287
3288
        // Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
3289
        $overhead = 8 + strlen($charset);
3290
3291
        if ('mail' === $this->Mailer) {
3292
            $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
3293
        } else {
3294
            $maxlen = static::MAX_LINE_LENGTH - $overhead;
3295
        }
3296
3297
        // Select the encoding that produces the shortest output and/or prevents corruption.
3298
        if ($matchcount > strlen($str) / 3) {
3299
            // More than 1/3 of the content needs encoding, use B-encode.
3300
            $encoding = 'B';
3301
        } elseif ($matchcount > 0) {
3302
            // Less than 1/3 of the content needs encoding, use Q-encode.
3303
            $encoding = 'Q';
3304
        } elseif (strlen($str) > $maxlen) {
3305
            // No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
3306
            $encoding = 'Q';
3307
        } else {
3308
            // No reformatting needed
3309
            $encoding = false;
3310
        }
3311
3312
        switch ($encoding) {
3313
            case 'B':
3314
                if ($this->hasMultiBytes($str)) {
3315
                    // Use a custom function which correctly encodes and wraps long
3316
                    // multibyte strings without breaking lines within a character
3317
                    $encoded = $this->base64EncodeWrapMB($str, "\n");
3318
                } else {
3319
                    $encoded = base64_encode($str);
3320
                    $maxlen -= $maxlen % 4;
3321
                    $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
3322
                }
3323
                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
3324
                break;
3325
            case 'Q':
3326
                $encoded = $this->encodeQ($str, $position);
3327
                $encoded = $this->wrapText($encoded, $maxlen, true);
3328
                $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
3329
                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
3330
                break;
3331
            default:
3332
                return $str;
3333
        }
3334
3335
        return trim(static::normalizeBreaks($encoded));
3336
    }
3337
3338
    /**
3339
     * Check if a string contains multi-byte characters.
3340
     *
3341
     * @param string $str multi-byte text to wrap encode
3342
     *
3343
     * @return bool
3344
     */
3345
    public function hasMultiBytes($str)
3346
    {
3347
        if (function_exists('mb_strlen')) {
3348
            return strlen($str) > mb_strlen($str, $this->CharSet);
3349
        }
3350
3351
        // Assume no multibytes (we can't handle without mbstring functions anyway)
3352
        return false;
3353
    }
3354
3355
    /**
3356
     * Does a string contain any 8-bit chars (in any charset)?
3357
     *
3358
     * @param string $text
3359
     *
3360
     * @return bool
3361
     */
3362
    public function has8bitChars($text)
3363
    {
3364
        return (bool) preg_match('/[\x80-\xFF]/', $text);
3365
    }
3366
3367
    /**
3368
     * Encode and wrap long multibyte strings for mail headers
3369
     * without breaking lines within a character.
3370
     * Adapted from a function by paravoid.
3371
     *
3372
     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
3373
     *
3374
     * @param string $str       multi-byte text to wrap encode
3375
     * @param string $linebreak string to use as linefeed/end-of-line
3376
     *
3377
     * @return string
3378
     */
3379
    public function base64EncodeWrapMB($str, $linebreak = null)
3380
    {
3381
        $start = '=?' . $this->CharSet . '?B?';
3382
        $end = '?=';
3383
        $encoded = '';
3384
        if (null === $linebreak) {
3385
            $linebreak = static::$LE;
3386
        }
3387
3388
        $mb_length = mb_strlen($str, $this->CharSet);
3389
        // Each line must have length <= 75, including $start and $end
3390
        $length = 75 - strlen($start) - strlen($end);
3391
        // Average multi-byte ratio
3392
        $ratio = $mb_length / strlen($str);
3393
        // Base64 has a 4:3 ratio
3394
        $avgLength = floor($length * $ratio * .75);
3395
3396
        $offset = 0;
0 ignored issues
show
Unused Code introduced by
$offset is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
4108
                            static::ENCODING_BASE64,
4109
                            static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
0 ignored issues
show
Bug introduced by
It seems like $filename defined by static::mb_pathinfo($url, PATHINFO_BASENAME) on line 4090 can also be of type array<string,string>; however, PHPMailer\PHPMailer\PHPMailer::mb_pathinfo() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
4110
                        )
4111
                    ) {
4112
                        $message = preg_replace(
4113
                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
4114
                            $images[1][$imgindex] . '="cid:' . $cid . '"',
4115
                            $message
4116
                        );
4117
                    }
4118
                }
4119
            }
4120
        }
4121
        $this->isHTML();
4122
        // Convert all message body line breaks to LE, makes quoted-printable encoding work much better
4123
        $this->Body = static::normalizeBreaks($message);
4124
        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
4125
        if (!$this->alternativeExists()) {
4126
            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
4127
                . static::$LE;
4128
        }
4129
4130
        return $this->Body;
4131
    }
4132
4133
    /**
4134
     * Convert an HTML string into plain text.
4135
     * This is used by msgHTML().
4136
     * Note - older versions of this function used a bundled advanced converter
4137
     * which was removed for license reasons in #232.
4138
     * Example usage:
4139
     *
4140
     * ```php
4141
     * // Use default conversion
4142
     * $plain = $mail->html2text($html);
4143
     * // Use your own custom converter
4144
     * $plain = $mail->html2text($html, function($html) {
4145
     *     $converter = new MyHtml2text($html);
4146
     *     return $converter->get_text();
4147
     * });
4148
     * ```
4149
     *
4150
     * @param string        $html     The HTML text to convert
4151
     * @param bool|callable $advanced Any boolean value to use the internal converter,
4152
     *                                or provide your own callable for custom conversion
4153
     *
4154
     * @return string
4155
     */
4156
    public function html2text($html, $advanced = false)
4157
    {
4158
        if (is_callable($advanced)) {
4159
            return call_user_func($advanced, $html);
4160
        }
4161
4162
        return html_entity_decode(
4163
            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
4164
            ENT_QUOTES,
4165
            $this->CharSet
4166
        );
4167
    }
4168
4169
    /**
4170
     * Get the MIME type for a file extension.
4171
     *
4172
     * @param string $ext File extension
4173
     *
4174
     * @return string MIME type of file
4175
     */
4176
    public static function _mime_types($ext = '')
4177
    {
4178
        $mimes = [
4179
            'xl' => 'application/excel',
4180
            'js' => 'application/javascript',
4181
            'hqx' => 'application/mac-binhex40',
4182
            'cpt' => 'application/mac-compactpro',
4183
            'bin' => 'application/macbinary',
4184
            'doc' => 'application/msword',
4185
            'word' => 'application/msword',
4186
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
4187
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
4188
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
4189
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
4190
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
4191
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
4192
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
4193
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
4194
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
4195
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
4196
            'class' => 'application/octet-stream',
4197
            'dll' => 'application/octet-stream',
4198
            'dms' => 'application/octet-stream',
4199
            'exe' => 'application/octet-stream',
4200
            'lha' => 'application/octet-stream',
4201
            'lzh' => 'application/octet-stream',
4202
            'psd' => 'application/octet-stream',
4203
            'sea' => 'application/octet-stream',
4204
            'so' => 'application/octet-stream',
4205
            'oda' => 'application/oda',
4206
            'pdf' => 'application/pdf',
4207
            'ai' => 'application/postscript',
4208
            'eps' => 'application/postscript',
4209
            'ps' => 'application/postscript',
4210
            'smi' => 'application/smil',
4211
            'smil' => 'application/smil',
4212
            'mif' => 'application/vnd.mif',
4213
            'xls' => 'application/vnd.ms-excel',
4214
            'ppt' => 'application/vnd.ms-powerpoint',
4215
            'wbxml' => 'application/vnd.wap.wbxml',
4216
            'wmlc' => 'application/vnd.wap.wmlc',
4217
            'dcr' => 'application/x-director',
4218
            'dir' => 'application/x-director',
4219
            'dxr' => 'application/x-director',
4220
            'dvi' => 'application/x-dvi',
4221
            'gtar' => 'application/x-gtar',
4222
            'php3' => 'application/x-httpd-php',
4223
            'php4' => 'application/x-httpd-php',
4224
            'php' => 'application/x-httpd-php',
4225
            'phtml' => 'application/x-httpd-php',
4226
            'phps' => 'application/x-httpd-php-source',
4227
            'swf' => 'application/x-shockwave-flash',
4228
            'sit' => 'application/x-stuffit',
4229
            'tar' => 'application/x-tar',
4230
            'tgz' => 'application/x-tar',
4231
            'xht' => 'application/xhtml+xml',
4232
            'xhtml' => 'application/xhtml+xml',
4233
            'zip' => 'application/zip',
4234
            'mid' => 'audio/midi',
4235
            'midi' => 'audio/midi',
4236
            'mp2' => 'audio/mpeg',
4237
            'mp3' => 'audio/mpeg',
4238
            'm4a' => 'audio/mp4',
4239
            'mpga' => 'audio/mpeg',
4240
            'aif' => 'audio/x-aiff',
4241
            'aifc' => 'audio/x-aiff',
4242
            'aiff' => 'audio/x-aiff',
4243
            'ram' => 'audio/x-pn-realaudio',
4244
            'rm' => 'audio/x-pn-realaudio',
4245
            'rpm' => 'audio/x-pn-realaudio-plugin',
4246
            'ra' => 'audio/x-realaudio',
4247
            'wav' => 'audio/x-wav',
4248
            'mka' => 'audio/x-matroska',
4249
            'bmp' => 'image/bmp',
4250
            'gif' => 'image/gif',
4251
            'jpeg' => 'image/jpeg',
4252
            'jpe' => 'image/jpeg',
4253
            'jpg' => 'image/jpeg',
4254
            'png' => 'image/png',
4255
            'tiff' => 'image/tiff',
4256
            'tif' => 'image/tiff',
4257
            'webp' => 'image/webp',
4258
            'avif' => 'image/avif',
4259
            'heif' => 'image/heif',
4260
            'heifs' => 'image/heif-sequence',
4261
            'heic' => 'image/heic',
4262
            'heics' => 'image/heic-sequence',
4263
            'eml' => 'message/rfc822',
4264
            'css' => 'text/css',
4265
            'html' => 'text/html',
4266
            'htm' => 'text/html',
4267
            'shtml' => 'text/html',
4268
            'log' => 'text/plain',
4269
            'text' => 'text/plain',
4270
            'txt' => 'text/plain',
4271
            'rtx' => 'text/richtext',
4272
            'rtf' => 'text/rtf',
4273
            'vcf' => 'text/vcard',
4274
            'vcard' => 'text/vcard',
4275
            'ics' => 'text/calendar',
4276
            'xml' => 'text/xml',
4277
            'xsl' => 'text/xml',
4278
            'wmv' => 'video/x-ms-wmv',
4279
            'mpeg' => 'video/mpeg',
4280
            'mpe' => 'video/mpeg',
4281
            'mpg' => 'video/mpeg',
4282
            'mp4' => 'video/mp4',
4283
            'm4v' => 'video/mp4',
4284
            'mov' => 'video/quicktime',
4285
            'qt' => 'video/quicktime',
4286
            'rv' => 'video/vnd.rn-realvideo',
4287
            'avi' => 'video/x-msvideo',
4288
            'movie' => 'video/x-sgi-movie',
4289
            'webm' => 'video/webm',
4290
            'mkv' => 'video/x-matroska',
4291
        ];
4292
        $ext = strtolower($ext);
4293
        if (array_key_exists($ext, $mimes)) {
4294
            return $mimes[$ext];
4295
        }
4296
4297
        return 'application/octet-stream';
4298
    }
4299
4300
    /**
4301
     * Map a file name to a MIME type.
4302
     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
4303
     *
4304
     * @param string $filename A file name or full path, does not need to exist as a file
4305
     *
4306
     * @return string
4307
     */
4308
    public static function filenameToType($filename)
4309
    {
4310
        // In case the path is a URL, strip any query string before getting extension
4311
        $qpos = strpos($filename, '?');
4312
        if (false !== $qpos) {
4313
            $filename = substr($filename, 0, $qpos);
4314
        }
4315
        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
4316
4317
        return static::_mime_types($ext);
0 ignored issues
show
Bug introduced by
It seems like $ext defined by static::mb_pathinfo($filename, PATHINFO_EXTENSION) on line 4315 can also be of type array<string,string>; however, PHPMailer\PHPMailer\PHPMailer::_mime_types() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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