Passed
Push — master ( 59dea8...e2e07a )
by Marcus
07:03
created

PHPMailer::encodeFile()   A

Complexity

Conditions 5
Paths 11

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.2408
c 0
b 0
f 0
cc 5
nc 11
nop 2
1
<?php
2
/**
3
 * PHPMailer - PHP email creation and transport class.
4
 * PHP Version 5.5.
5
 *
6
 * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
7
 *
8
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
9
 * @author    Jim Jagielski (jimjag) <[email protected]>
10
 * @author    Andy Prevost (codeworxtech) <[email protected]>
11
 * @author    Brent R. Matzelle (original founder)
12
 * @copyright 2012 - 2020 Marcus Bointon
13
 * @copyright 2010 - 2012 Jim Jagielski
14
 * @copyright 2004 - 2009 Andy Prevost
15
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
16
 * @note      This program is distributed in the hope that it will be useful - WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18
 * FITNESS FOR A PARTICULAR PURPOSE.
19
 */
20
21
namespace PHPMailer\PHPMailer;
22
23
/**
24
 * PHPMailer - PHP email creation and transport class.
25
 *
26
 * @author Marcus Bointon (Synchro/coolbru) <[email protected]>
27
 * @author Jim Jagielski (jimjag) <[email protected]>
28
 * @author Andy Prevost (codeworxtech) <[email protected]>
29
 * @author Brent R. Matzelle (original founder)
30
 */
31
class PHPMailer
32
{
33
    const CHARSET_ASCII = 'us-ascii';
34
    const CHARSET_ISO88591 = 'iso-8859-1';
35
    const CHARSET_UTF8 = 'utf-8';
36
37
    const CONTENT_TYPE_PLAINTEXT = 'text/plain';
38
    const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';
39
    const CONTENT_TYPE_TEXT_HTML = 'text/html';
40
    const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
41
    const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
42
    const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
43
44
    const ENCODING_7BIT = '7bit';
45
    const ENCODING_8BIT = '8bit';
46
    const ENCODING_BASE64 = 'base64';
47
    const ENCODING_BINARY = 'binary';
48
    const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
49
50
    const ENCRYPTION_STARTTLS = 'tls';
51
    const ENCRYPTION_SMTPS = 'ssl';
52
53
    const ICAL_METHOD_REQUEST = 'REQUEST';
54
    const ICAL_METHOD_PUBLISH = 'PUBLISH';
55
    const ICAL_METHOD_REPLY = 'REPLY';
56
    const ICAL_METHOD_ADD = 'ADD';
57
    const ICAL_METHOD_CANCEL = 'CANCEL';
58
    const ICAL_METHOD_REFRESH = 'REFRESH';
59
    const ICAL_METHOD_COUNTER = 'COUNTER';
60
    const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
61
62
    /**
63
     * Email priority.
64
     * Options: null (default), 1 = High, 3 = Normal, 5 = low.
65
     * When null, the header is not set at all.
66
     *
67
     * @var int|null
68
     */
69
    public $Priority;
70
71
    /**
72
     * The character set of the message.
73
     *
74
     * @var string
75
     */
76
    public $CharSet = self::CHARSET_ISO88591;
77
78
    /**
79
     * The MIME Content-type of the message.
80
     *
81
     * @var string
82
     */
83
    public $ContentType = self::CONTENT_TYPE_PLAINTEXT;
84
85
    /**
86
     * The message encoding.
87
     * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
88
     *
89
     * @var string
90
     */
91
    public $Encoding = self::ENCODING_8BIT;
92
93
    /**
94
     * Holds the most recent mailer error message.
95
     *
96
     * @var string
97
     */
98
    public $ErrorInfo = '';
99
100
    /**
101
     * The From email address for the message.
102
     *
103
     * @var string
104
     */
105
    public $From = 'root@localhost';
106
107
    /**
108
     * The From name of the message.
109
     *
110
     * @var string
111
     */
112
    public $FromName = 'Root User';
113
114
    /**
115
     * The envelope sender of the message.
116
     * This will usually be turned into a Return-Path header by the receiver,
117
     * and is the address that bounces will be sent to.
118
     * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
119
     *
120
     * @var string
121
     */
122
    public $Sender = '';
123
124
    /**
125
     * The Subject of the message.
126
     *
127
     * @var string
128
     */
129
    public $Subject = '';
130
131
    /**
132
     * An HTML or plain text message body.
133
     * If HTML then call isHTML(true).
134
     *
135
     * @var string
136
     */
137
    public $Body = '';
138
139
    /**
140
     * The plain-text message body.
141
     * This body can be read by mail clients that do not have HTML email
142
     * capability such as mutt & Eudora.
143
     * Clients that can read HTML will view the normal Body.
144
     *
145
     * @var string
146
     */
147
    public $AltBody = '';
148
149
    /**
150
     * An iCal message part body.
151
     * Only supported in simple alt or alt_inline message types
152
     * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
153
     *
154
     * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/
155
     * @see http://kigkonsult.se/iCalcreator/
156
     *
157
     * @var string
158
     */
159
    public $Ical = '';
160
161
    /**
162
     * Value-array of "method" in Contenttype header "text/calendar"
163
     *
164
     * @var string[]
165
     */
166
    protected static $IcalMethods = [
167
        self::ICAL_METHOD_REQUEST,
168
        self::ICAL_METHOD_PUBLISH,
169
        self::ICAL_METHOD_REPLY,
170
        self::ICAL_METHOD_ADD,
171
        self::ICAL_METHOD_CANCEL,
172
        self::ICAL_METHOD_REFRESH,
173
        self::ICAL_METHOD_COUNTER,
174
        self::ICAL_METHOD_DECLINECOUNTER,
175
    ];
176
177
    /**
178
     * The complete compiled MIME message body.
179
     *
180
     * @var string
181
     */
182
    protected $MIMEBody = '';
183
184
    /**
185
     * The complete compiled MIME message headers.
186
     *
187
     * @var string
188
     */
189
    protected $MIMEHeader = '';
190
191
    /**
192
     * Extra headers that createHeader() doesn't fold in.
193
     *
194
     * @var string
195
     */
196
    protected $mailHeader = '';
197
198
    /**
199
     * Word-wrap the message body to this number of chars.
200
     * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
201
     *
202
     * @see static::STD_LINE_LENGTH
203
     *
204
     * @var int
205
     */
206
    public $WordWrap = 0;
207
208
    /**
209
     * Which method to use to send mail.
210
     * Options: "mail", "sendmail", or "smtp".
211
     *
212
     * @var string
213
     */
214
    public $Mailer = 'mail';
215
216
    /**
217
     * The path to the sendmail program.
218
     *
219
     * @var string
220
     */
221
    public $Sendmail = '/usr/sbin/sendmail';
222
223
    /**
224
     * Whether mail() uses a fully sendmail-compatible MTA.
225
     * One which supports sendmail's "-oi -f" options.
226
     *
227
     * @var bool
228
     */
229
    public $UseSendmailOptions = true;
230
231
    /**
232
     * The email address that a reading confirmation should be sent to, also known as read receipt.
233
     *
234
     * @var string
235
     */
236
    public $ConfirmReadingTo = '';
237
238
    /**
239
     * The hostname to use in the Message-ID header and as default HELO string.
240
     * If empty, PHPMailer attempts to find one with, in order,
241
     * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
242
     * 'localhost.localdomain'.
243
     *
244
     * @see PHPMailer::$Helo
245
     *
246
     * @var string
247
     */
248
    public $Hostname = '';
249
250
    /**
251
     * An ID to be used in the Message-ID header.
252
     * If empty, a unique id will be generated.
253
     * You can set your own, but it must be in the format "<id@domain>",
254
     * as defined in RFC5322 section 3.6.4 or it will be ignored.
255
     *
256
     * @see https://tools.ietf.org/html/rfc5322#section-3.6.4
257
     *
258
     * @var string
259
     */
260
    public $MessageID = '';
261
262
    /**
263
     * The message Date to be used in the Date header.
264
     * If empty, the current date will be added.
265
     *
266
     * @var string
267
     */
268
    public $MessageDate = '';
269
270
    /**
271
     * SMTP hosts.
272
     * Either a single hostname or multiple semicolon-delimited hostnames.
273
     * You can also specify a different port
274
     * for each host by using this format: [hostname:port]
275
     * (e.g. "smtp1.example.com:25;smtp2.example.com").
276
     * You can also specify encryption type, for example:
277
     * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
278
     * Hosts will be tried in order.
279
     *
280
     * @var string
281
     */
282
    public $Host = 'localhost';
283
284
    /**
285
     * The default SMTP server port.
286
     *
287
     * @var int
288
     */
289
    public $Port = 25;
290
291
    /**
292
     * The SMTP HELO/EHLO name used for the SMTP connection.
293
     * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
294
     * one with the same method described above for $Hostname.
295
     *
296
     * @see PHPMailer::$Hostname
297
     *
298
     * @var string
299
     */
300
    public $Helo = '';
301
302
    /**
303
     * What kind of encryption to use on the SMTP connection.
304
     * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
305
     *
306
     * @var string
307
     */
308
    public $SMTPSecure = '';
309
310
    /**
311
     * Whether to enable TLS encryption automatically if a server supports it,
312
     * even if `SMTPSecure` is not set to 'tls'.
313
     * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
314
     *
315
     * @var bool
316
     */
317
    public $SMTPAutoTLS = true;
318
319
    /**
320
     * Whether to use SMTP authentication.
321
     * Uses the Username and Password properties.
322
     *
323
     * @see PHPMailer::$Username
324
     * @see PHPMailer::$Password
325
     *
326
     * @var bool
327
     */
328
    public $SMTPAuth = false;
329
330
    /**
331
     * Options array passed to stream_context_create when connecting via SMTP.
332
     *
333
     * @var array
334
     */
335
    public $SMTPOptions = [];
336
337
    /**
338
     * SMTP username.
339
     *
340
     * @var string
341
     */
342
    public $Username = '';
343
344
    /**
345
     * SMTP password.
346
     *
347
     * @var string
348
     */
349
    public $Password = '';
350
351
    /**
352
     * SMTP auth type.
353
     * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified.
354
     *
355
     * @var string
356
     */
357
    public $AuthType = '';
358
359
    /**
360
     * An instance of the PHPMailer OAuth class.
361
     *
362
     * @var OAuth
363
     */
364
    protected $oauth;
365
366
    /**
367
     * The SMTP server timeout in seconds.
368
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
369
     *
370
     * @var int
371
     */
372
    public $Timeout = 300;
373
374
    /**
375
     * Comma separated list of DSN notifications
376
     * 'NEVER' under no circumstances a DSN must be returned to the sender.
377
     *         If you use NEVER all other notifications will be ignored.
378
     * 'SUCCESS' will notify you when your mail has arrived at its destination.
379
     * 'FAILURE' will arrive if an error occurred during delivery.
380
     * 'DELAY'   will notify you if there is an unusual delay in delivery, but the actual
381
     *           delivery's outcome (success or failure) is not yet decided.
382
     *
383
     * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
384
     */
385
    public $dsn = '';
386
387
    /**
388
     * SMTP class debug output mode.
389
     * Debug output level.
390
     * Options:
391
     * * SMTP::DEBUG_OFF: No output
392
     * * SMTP::DEBUG_CLIENT: Client messages
393
     * * SMTP::DEBUG_SERVER: Client and server messages
394
     * * SMTP::DEBUG_CONNECTION: As SERVER plus connection status
395
     * * SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
396
     *
397
     * @see SMTP::$do_debug
398
     *
399
     * @var int
400
     */
401
    public $SMTPDebug = 0;
402
403
    /**
404
     * How to handle debug output.
405
     * Options:
406
     * * `echo` Output plain-text as-is, appropriate for CLI
407
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
408
     * * `error_log` Output to error log as configured in php.ini
409
     * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
410
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
411
     *
412
     * ```php
413
     * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
414
     * ```
415
     *
416
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
417
     * level output is used:
418
     *
419
     * ```php
420
     * $mail->Debugoutput = new myPsr3Logger;
421
     * ```
422
     *
423
     * @see SMTP::$Debugoutput
424
     *
425
     * @var string|callable|\Psr\Log\LoggerInterface
426
     */
427
    public $Debugoutput = 'echo';
428
429
    /**
430
     * Whether to keep SMTP connection open after each message.
431
     * If this is set to true then to close the connection
432
     * requires an explicit call to smtpClose().
433
     *
434
     * @var bool
435
     */
436
    public $SMTPKeepAlive = false;
437
438
    /**
439
     * Whether to split multiple to addresses into multiple messages
440
     * or send them all in one message.
441
     * Only supported in `mail` and `sendmail` transports, not in SMTP.
442
     *
443
     * @var bool
444
     *
445
     * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
446
     */
447
    public $SingleTo = false;
448
449
    /**
450
     * Storage for addresses when SingleTo is enabled.
451
     *
452
     * @var array
453
     */
454
    protected $SingleToArray = [];
455
456
    /**
457
     * Whether to generate VERP addresses on send.
458
     * Only applicable when sending via SMTP.
459
     *
460
     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
461
     * @see http://www.postfix.org/VERP_README.html Postfix VERP info
462
     *
463
     * @var bool
464
     */
465
    public $do_verp = false;
466
467
    /**
468
     * Whether to allow sending messages with an empty body.
469
     *
470
     * @var bool
471
     */
472
    public $AllowEmpty = false;
473
474
    /**
475
     * DKIM selector.
476
     *
477
     * @var string
478
     */
479
    public $DKIM_selector = '';
480
481
    /**
482
     * DKIM Identity.
483
     * Usually the email address used as the source of the email.
484
     *
485
     * @var string
486
     */
487
    public $DKIM_identity = '';
488
489
    /**
490
     * DKIM passphrase.
491
     * Used if your key is encrypted.
492
     *
493
     * @var string
494
     */
495
    public $DKIM_passphrase = '';
496
497
    /**
498
     * DKIM signing domain name.
499
     *
500
     * @example 'example.com'
501
     *
502
     * @var string
503
     */
504
    public $DKIM_domain = '';
505
506
    /**
507
     * DKIM Copy header field values for diagnostic use.
508
     *
509
     * @var bool
510
     */
511
    public $DKIM_copyHeaderFields = true;
512
513
    /**
514
     * DKIM Extra signing headers.
515
     *
516
     * @example ['List-Unsubscribe', 'List-Help']
517
     *
518
     * @var array
519
     */
520
    public $DKIM_extraHeaders = [];
521
522
    /**
523
     * DKIM private key file path.
524
     *
525
     * @var string
526
     */
527
    public $DKIM_private = '';
528
529
    /**
530
     * DKIM private key string.
531
     *
532
     * If set, takes precedence over `$DKIM_private`.
533
     *
534
     * @var string
535
     */
536
    public $DKIM_private_string = '';
537
538
    /**
539
     * Callback Action function name.
540
     *
541
     * The function that handles the result of the send email action.
542
     * It is called out by send() for each email sent.
543
     *
544
     * Value can be any php callable: http://www.php.net/is_callable
545
     *
546
     * Parameters:
547
     *   bool $result        result of the send action
548
     *   array   $to            email addresses of the recipients
549
     *   array   $cc            cc email addresses
550
     *   array   $bcc           bcc email addresses
551
     *   string  $subject       the subject
552
     *   string  $body          the email body
553
     *   string  $from          email address of sender
554
     *   string  $extra         extra information of possible use
555
     *                          "smtp_transaction_id' => last smtp transaction id
556
     *
557
     * @var string
558
     */
559
    public $action_function = '';
560
561
    /**
562
     * What to put in the X-Mailer header.
563
     * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
564
     *
565
     * @var string|null
566
     */
567
    public $XMailer = '';
568
569
    /**
570
     * Which validator to use by default when validating email addresses.
571
     * May be a callable to inject your own validator, but there are several built-in validators.
572
     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
573
     *
574
     * @see PHPMailer::validateAddress()
575
     *
576
     * @var string|callable
577
     */
578
    public static $validator = 'php';
579
580
    /**
581
     * An instance of the SMTP sender class.
582
     *
583
     * @var SMTP
584
     */
585
    protected $smtp;
586
587
    /**
588
     * The array of 'to' names and addresses.
589
     *
590
     * @var array
591
     */
592
    protected $to = [];
593
594
    /**
595
     * The array of 'cc' names and addresses.
596
     *
597
     * @var array
598
     */
599
    protected $cc = [];
600
601
    /**
602
     * The array of 'bcc' names and addresses.
603
     *
604
     * @var array
605
     */
606
    protected $bcc = [];
607
608
    /**
609
     * The array of reply-to names and addresses.
610
     *
611
     * @var array
612
     */
613
    protected $ReplyTo = [];
614
615
    /**
616
     * An array of all kinds of addresses.
617
     * Includes all of $to, $cc, $bcc.
618
     *
619
     * @see PHPMailer::$to
620
     * @see PHPMailer::$cc
621
     * @see PHPMailer::$bcc
622
     *
623
     * @var array
624
     */
625
    protected $all_recipients = [];
626
627
    /**
628
     * An array of names and addresses queued for validation.
629
     * In send(), valid and non duplicate entries are moved to $all_recipients
630
     * and one of $to, $cc, or $bcc.
631
     * This array is used only for addresses with IDN.
632
     *
633
     * @see PHPMailer::$to
634
     * @see PHPMailer::$cc
635
     * @see PHPMailer::$bcc
636
     * @see PHPMailer::$all_recipients
637
     *
638
     * @var array
639
     */
640
    protected $RecipientsQueue = [];
641
642
    /**
643
     * An array of reply-to names and addresses queued for validation.
644
     * In send(), valid and non duplicate entries are moved to $ReplyTo.
645
     * This array is used only for addresses with IDN.
646
     *
647
     * @see PHPMailer::$ReplyTo
648
     *
649
     * @var array
650
     */
651
    protected $ReplyToQueue = [];
652
653
    /**
654
     * The array of attachments.
655
     *
656
     * @var array
657
     */
658
    protected $attachment = [];
659
660
    /**
661
     * The array of custom headers.
662
     *
663
     * @var array
664
     */
665
    protected $CustomHeader = [];
666
667
    /**
668
     * The most recent Message-ID (including angular brackets).
669
     *
670
     * @var string
671
     */
672
    protected $lastMessageID = '';
673
674
    /**
675
     * The message's MIME type.
676
     *
677
     * @var string
678
     */
679
    protected $message_type = '';
680
681
    /**
682
     * The array of MIME boundary strings.
683
     *
684
     * @var array
685
     */
686
    protected $boundary = [];
687
688
    /**
689
     * The array of available languages.
690
     *
691
     * @var array
692
     */
693
    protected $language = [];
694
695
    /**
696
     * The number of errors encountered.
697
     *
698
     * @var int
699
     */
700
    protected $error_count = 0;
701
702
    /**
703
     * The S/MIME certificate file path.
704
     *
705
     * @var string
706
     */
707
    protected $sign_cert_file = '';
708
709
    /**
710
     * The S/MIME key file path.
711
     *
712
     * @var string
713
     */
714
    protected $sign_key_file = '';
715
716
    /**
717
     * The optional S/MIME extra certificates ("CA Chain") file path.
718
     *
719
     * @var string
720
     */
721
    protected $sign_extracerts_file = '';
722
723
    /**
724
     * The S/MIME password for the key.
725
     * Used only if the key is encrypted.
726
     *
727
     * @var string
728
     */
729
    protected $sign_key_pass = '';
730
731
    /**
732
     * Whether to throw exceptions for errors.
733
     *
734
     * @var bool
735
     */
736
    protected $exceptions = false;
737
738
    /**
739
     * Unique ID used for message ID and boundaries.
740
     *
741
     * @var string
742
     */
743
    protected $uniqueid = '';
744
745
    /**
746
     * The PHPMailer Version number.
747
     *
748
     * @var string
749
     */
750
    const VERSION = '6.1.7';
751
752
    /**
753
     * Error severity: message only, continue processing.
754
     *
755
     * @var int
756
     */
757
    const STOP_MESSAGE = 0;
758
759
    /**
760
     * Error severity: message, likely ok to continue processing.
761
     *
762
     * @var int
763
     */
764
    const STOP_CONTINUE = 1;
765
766
    /**
767
     * Error severity: message, plus full stop, critical error reached.
768
     *
769
     * @var int
770
     */
771
    const STOP_CRITICAL = 2;
772
773
    /**
774
     * The SMTP standard CRLF line break.
775
     * If you want to change line break format, change static::$LE, not this.
776
     */
777
    const CRLF = "\r\n";
778
779
    /**
780
     * "Folding White Space" a white space string used for line folding.
781
     */
782
    const FWS = ' ';
783
784
    /**
785
     * SMTP RFC standard line ending; Carriage Return, Line Feed.
786
     *
787
     * @var string
788
     */
789
    protected static $LE = self::CRLF;
790
791
    /**
792
     * The maximum line length supported by mail().
793
     *
794
     * Background: mail() will sometimes corrupt messages
795
     * with headers headers longer than 65 chars, see #818.
796
     *
797
     * @var int
798
     */
799
    const MAIL_MAX_LINE_LENGTH = 63;
800
801
    /**
802
     * The maximum line length allowed by RFC 2822 section 2.1.1.
803
     *
804
     * @var int
805
     */
806
    const MAX_LINE_LENGTH = 998;
807
808
    /**
809
     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
810
     * This length does NOT include the line break
811
     * 76 means that lines will be 77 or 78 chars depending on whether
812
     * the line break format is LF or CRLF; both are valid.
813
     *
814
     * @var int
815
     */
816
    const STD_LINE_LENGTH = 76;
817
818
    /**
819
     * Constructor.
820
     *
821
     * @param bool $exceptions Should we throw external exceptions?
822
     */
823
    public function __construct($exceptions = null)
824
    {
825
        if (null !== $exceptions) {
826
            $this->exceptions = (bool) $exceptions;
827
        }
828
        //Pick an appropriate debug output format automatically
829
        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
830
    }
831
832
    /**
833
     * Destructor.
834
     */
835
    public function __destruct()
836
    {
837
        //Close any open SMTP connection nicely
838
        $this->smtpClose();
839
    }
840
841
    /**
842
     * Call mail() in a safe_mode-aware fashion.
843
     * Also, unless sendmail_path points to sendmail (or something that
844
     * claims to be sendmail), don't pass params (not a perfect fix,
845
     * but it will do).
846
     *
847
     * @param string      $to      To
848
     * @param string      $subject Subject
849
     * @param string      $body    Message Body
850
     * @param string      $header  Additional Header(s)
851
     * @param string|null $params  Params
852
     *
853
     * @return bool
854
     */
855
    private function mailPassthru($to, $subject, $body, $header, $params)
856
    {
857
        //Check overloading of mail function to avoid double-encoding
858
        if (ini_get('mbstring.func_overload') & 1) {
859
            $subject = $this->secureHeader($subject);
860
        } else {
861
            $subject = $this->encodeHeader($this->secureHeader($subject));
862
        }
863
        //Calling mail() with null params breaks
864
        if (!$this->UseSendmailOptions || null === $params) {
865
            $result = @mail($to, $subject, $body, $header);
866
        } else {
867
            $result = @mail($to, $subject, $body, $header, $params);
868
        }
869
870
        return $result;
871
    }
872
873
    /**
874
     * Output debugging info via user-defined method.
875
     * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug).
876
     *
877
     * @see PHPMailer::$Debugoutput
878
     * @see PHPMailer::$SMTPDebug
879
     *
880
     * @param string $str
881
     */
882
    protected function edebug($str)
883
    {
884
        if ($this->SMTPDebug <= 0) {
885
            return;
886
        }
887
        //Is this a PSR-3 logger?
888
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
889
            $this->Debugoutput->debug($str);
890
891
            return;
892
        }
893
        //Avoid clash with built-in function names
894
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
895
            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
896
897
            return;
898
        }
899
        switch ($this->Debugoutput) {
900
            case 'error_log':
901
                //Don't output, just log
902
                /** @noinspection ForgottenDebugOutputInspection */
903
                error_log($str);
904
                break;
905
            case 'html':
906
                //Cleans up output a bit for a better looking, HTML-safe output
907
                echo htmlentities(
908
                    preg_replace('/[\r\n]+/', '', $str),
909
                    ENT_QUOTES,
910
                    'UTF-8'
911
                ), "<br>\n";
912
                break;
913
            case 'echo':
914
            default:
915
                //Normalize line breaks
916
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
917
                echo gmdate('Y-m-d H:i:s'),
918
                "\t",
919
                    //Trim trailing space
920
                trim(
921
                    //Indent for readability, except for trailing break
922
                    str_replace(
923
                        "\n",
924
                        "\n                   \t                  ",
925
                        trim($str)
926
                    )
927
                ),
928
                "\n";
929
        }
930
    }
931
932
    /**
933
     * Sets message type to HTML or plain.
934
     *
935
     * @param bool $isHtml True for HTML mode
936
     */
937
    public function isHTML($isHtml = true)
938
    {
939
        if ($isHtml) {
940
            $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
941
        } else {
942
            $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
943
        }
944
    }
945
946
    /**
947
     * Send messages using SMTP.
948
     */
949
    public function isSMTP()
950
    {
951
        $this->Mailer = 'smtp';
952
    }
953
954
    /**
955
     * Send messages using PHP's mail() function.
956
     */
957
    public function isMail()
958
    {
959
        $this->Mailer = 'mail';
960
    }
961
962
    /**
963
     * Send messages using $Sendmail.
964
     */
965
    public function isSendmail()
966
    {
967
        $ini_sendmail_path = ini_get('sendmail_path');
968
969
        if (false === stripos($ini_sendmail_path, 'sendmail')) {
970
            $this->Sendmail = '/usr/sbin/sendmail';
971
        } else {
972
            $this->Sendmail = $ini_sendmail_path;
973
        }
974
        $this->Mailer = 'sendmail';
975
    }
976
977
    /**
978
     * Send messages using qmail.
979
     */
980
    public function isQmail()
981
    {
982
        $ini_sendmail_path = ini_get('sendmail_path');
983
984
        if (false === stripos($ini_sendmail_path, 'qmail')) {
985
            $this->Sendmail = '/var/qmail/bin/qmail-inject';
986
        } else {
987
            $this->Sendmail = $ini_sendmail_path;
988
        }
989
        $this->Mailer = 'qmail';
990
    }
991
992
    /**
993
     * Add a "To" address.
994
     *
995
     * @param string $address The email address to send to
996
     * @param string $name
997
     *
998
     * @throws Exception
999
     *
1000
     * @return bool true on success, false if address already used or invalid in some way
1001
     */
1002
    public function addAddress($address, $name = '')
1003
    {
1004
        return $this->addOrEnqueueAnAddress('to', $address, $name);
1005
    }
1006
1007
    /**
1008
     * Add a "CC" address.
1009
     *
1010
     * @param string $address The email address to send to
1011
     * @param string $name
1012
     *
1013
     * @throws Exception
1014
     *
1015
     * @return bool true on success, false if address already used or invalid in some way
1016
     */
1017
    public function addCC($address, $name = '')
1018
    {
1019
        return $this->addOrEnqueueAnAddress('cc', $address, $name);
1020
    }
1021
1022
    /**
1023
     * Add a "BCC" address.
1024
     *
1025
     * @param string $address The email address to send to
1026
     * @param string $name
1027
     *
1028
     * @throws Exception
1029
     *
1030
     * @return bool true on success, false if address already used or invalid in some way
1031
     */
1032
    public function addBCC($address, $name = '')
1033
    {
1034
        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
1035
    }
1036
1037
    /**
1038
     * Add a "Reply-To" address.
1039
     *
1040
     * @param string $address The email address to reply to
1041
     * @param string $name
1042
     *
1043
     * @throws Exception
1044
     *
1045
     * @return bool true on success, false if address already used or invalid in some way
1046
     */
1047
    public function addReplyTo($address, $name = '')
1048
    {
1049
        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
1050
    }
1051
1052
    /**
1053
     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
1054
     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
1055
     * be modified after calling this function), addition of such addresses is delayed until send().
1056
     * Addresses that have been added already return false, but do not throw exceptions.
1057
     *
1058
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1059
     * @param string $address The email address to send, resp. to reply to
1060
     * @param string $name
1061
     *
1062
     * @throws Exception
1063
     *
1064
     * @return bool true on success, false if address already used or invalid in some way
1065
     */
1066
    protected function addOrEnqueueAnAddress($kind, $address, $name)
1067
    {
1068
        $address = trim($address);
1069
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1070
        $pos = strrpos($address, '@');
1071
        if (false === $pos) {
1072
            // At-sign is missing.
1073
            $error_message = sprintf(
1074
                '%s (%s): %s',
1075
                $this->lang('invalid_address'),
1076
                $kind,
1077
                $address
1078
            );
1079
            $this->setError($error_message);
1080
            $this->edebug($error_message);
1081
            if ($this->exceptions) {
1082
                throw new Exception($error_message);
1083
            }
1084
1085
            return false;
1086
        }
1087
        $params = [$kind, $address, $name];
1088
        // Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
1089
        if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
1090
            if ('Reply-To' !== $kind) {
1091
                if (!array_key_exists($address, $this->RecipientsQueue)) {
1092
                    $this->RecipientsQueue[$address] = $params;
1093
1094
                    return true;
1095
                }
1096
            } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
1097
                $this->ReplyToQueue[$address] = $params;
1098
1099
                return true;
1100
            }
1101
1102
            return false;
1103
        }
1104
1105
        // Immediately add standard addresses without IDN.
1106
        return call_user_func_array([$this, 'addAnAddress'], $params);
1107
    }
1108
1109
    /**
1110
     * Add an address to one of the recipient arrays or to the ReplyTo array.
1111
     * Addresses that have been added already return false, but do not throw exceptions.
1112
     *
1113
     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
1114
     * @param string $address The email address to send, resp. to reply to
1115
     * @param string $name
1116
     *
1117
     * @throws Exception
1118
     *
1119
     * @return bool true on success, false if address already used or invalid in some way
1120
     */
1121
    protected function addAnAddress($kind, $address, $name = '')
1122
    {
1123
        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
1124
            $error_message = sprintf(
1125
                '%s: %s',
1126
                $this->lang('Invalid recipient kind'),
1127
                $kind
1128
            );
1129
            $this->setError($error_message);
1130
            $this->edebug($error_message);
1131
            if ($this->exceptions) {
1132
                throw new Exception($error_message);
1133
            }
1134
1135
            return false;
1136
        }
1137
        if (!static::validateAddress($address)) {
1138
            $error_message = sprintf(
1139
                '%s (%s): %s',
1140
                $this->lang('invalid_address'),
1141
                $kind,
1142
                $address
1143
            );
1144
            $this->setError($error_message);
1145
            $this->edebug($error_message);
1146
            if ($this->exceptions) {
1147
                throw new Exception($error_message);
1148
            }
1149
1150
            return false;
1151
        }
1152
        if ('Reply-To' !== $kind) {
1153
            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
1154
                $this->{$kind}[] = [$address, $name];
1155
                $this->all_recipients[strtolower($address)] = true;
1156
1157
                return true;
1158
            }
1159
        } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
1160
            $this->ReplyTo[strtolower($address)] = [$address, $name];
1161
1162
            return true;
1163
        }
1164
1165
        return false;
1166
    }
1167
1168
    /**
1169
     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
1170
     * of the form "display name <address>" into an array of name/address pairs.
1171
     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
1172
     * Note that quotes in the name part are removed.
1173
     *
1174
     * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
1175
     *
1176
     * @param string $addrstr The address list string
1177
     * @param bool   $useimap Whether to use the IMAP extension to parse the list
1178
     *
1179
     * @return array
1180
     */
1181
    public static function parseAddresses($addrstr, $useimap = true)
1182
    {
1183
        $addresses = [];
1184
        if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
1185
            //Use this built-in parser if it's available
1186
            $list = imap_rfc822_parse_adrlist($addrstr, '');
1187
            foreach ($list as $address) {
1188
                if (('.SYNTAX-ERROR.' !== $address->host) && static::validateAddress(
1189
                    $address->mailbox . '@' . $address->host
1190
                )) {
1191
                    $addresses[] = [
1192
                        'name' => (property_exists($address, 'personal') ? $address->personal : ''),
1193
                        'address' => $address->mailbox . '@' . $address->host,
1194
                    ];
1195
                }
1196
            }
1197
        } else {
1198
            //Use this simpler parser
1199
            $list = explode(',', $addrstr);
1200
            foreach ($list as $address) {
1201
                $address = trim($address);
1202
                //Is there a separate name part?
1203
                if (strpos($address, '<') === false) {
1204
                    //No separate name, just use the whole thing
1205
                    if (static::validateAddress($address)) {
1206
                        $addresses[] = [
1207
                            'name' => '',
1208
                            'address' => $address,
1209
                        ];
1210
                    }
1211
                } else {
1212
                    list($name, $email) = explode('<', $address);
1213
                    $email = trim(str_replace('>', '', $email));
1214
                    if (static::validateAddress($email)) {
1215
                        $addresses[] = [
1216
                            'name' => trim(str_replace(['"', "'"], '', $name)),
1217
                            'address' => $email,
1218
                        ];
1219
                    }
1220
                }
1221
            }
1222
        }
1223
1224
        return $addresses;
1225
    }
1226
1227
    /**
1228
     * Set the From and FromName properties.
1229
     *
1230
     * @param string $address
1231
     * @param string $name
1232
     * @param bool   $auto    Whether to also set the Sender address, defaults to true
1233
     *
1234
     * @throws Exception
1235
     *
1236
     * @return bool
1237
     */
1238
    public function setFrom($address, $name = '', $auto = true)
1239
    {
1240
        $address = trim($address);
1241
        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
1242
        // Don't validate now addresses with IDN. Will be done in send().
1243
        $pos = strrpos($address, '@');
1244
        if ((false === $pos)
1245
            || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
1246
            && !static::validateAddress($address))
1247
        ) {
1248
            $error_message = sprintf(
1249
                '%s (From): %s',
1250
                $this->lang('invalid_address'),
1251
                $address
1252
            );
1253
            $this->setError($error_message);
1254
            $this->edebug($error_message);
1255
            if ($this->exceptions) {
1256
                throw new Exception($error_message);
1257
            }
1258
1259
            return false;
1260
        }
1261
        $this->From = $address;
1262
        $this->FromName = $name;
1263
        if ($auto && empty($this->Sender)) {
1264
            $this->Sender = $address;
1265
        }
1266
1267
        return true;
1268
    }
1269
1270
    /**
1271
     * Return the Message-ID header of the last email.
1272
     * Technically this is the value from the last time the headers were created,
1273
     * but it's also the message ID of the last sent message except in
1274
     * pathological cases.
1275
     *
1276
     * @return string
1277
     */
1278
    public function getLastMessageID()
1279
    {
1280
        return $this->lastMessageID;
1281
    }
1282
1283
    /**
1284
     * Check that a string looks like an email address.
1285
     * Validation patterns supported:
1286
     * * `auto` Pick best pattern automatically;
1287
     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
1288
     * * `pcre` Use old PCRE implementation;
1289
     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
1290
     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
1291
     * * `noregex` Don't use a regex: super fast, really dumb.
1292
     * Alternatively you may pass in a callable to inject your own validator, for example:
1293
     *
1294
     * ```php
1295
     * PHPMailer::validateAddress('[email protected]', function($address) {
1296
     *     return (strpos($address, '@') !== false);
1297
     * });
1298
     * ```
1299
     *
1300
     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
1301
     *
1302
     * @param string          $address       The email address to check
1303
     * @param string|callable $patternselect Which pattern to use
1304
     *
1305
     * @return bool
1306
     */
1307
    public static function validateAddress($address, $patternselect = null)
1308
    {
1309
        if (null === $patternselect) {
1310
            $patternselect = static::$validator;
1311
        }
1312
        if (is_callable($patternselect)) {
1313
            return call_user_func($patternselect, $address);
1314
        }
1315
        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
1316
        if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
1317
            return false;
1318
        }
1319
        switch ($patternselect) {
1320
            case 'pcre': //Kept for BC
1321
            case 'pcre8':
1322
                /*
1323
                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
1324
                 * is based.
1325
                 * In addition to the addresses allowed by filter_var, also permits:
1326
                 *  * dotless domains: `a@b`
1327
                 *  * comments: `1234 @ local(blah) .machine .example`
1328
                 *  * quoted elements: `'"test blah"@example.org'`
1329
                 *  * numeric TLDs: `[email protected]`
1330
                 *  * unbracketed IPv4 literals: `[email protected]`
1331
                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'
1332
                 * Not all of these will necessarily work for sending!
1333
                 *
1334
                 * @see       http://squiloople.com/2009/12/20/email-address-validation/
1335
                 * @copyright 2009-2010 Michael Rushton
1336
                 * Feel free to use and redistribute this code. But please keep this copyright notice.
1337
                 */
1338
                return (bool) preg_match(
1339
                    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
1340
                    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
1341
                    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
1342
                    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
1343
                    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
1344
                    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
1345
                    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
1346
                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
1347
                    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
1348
                    $address
1349
                );
1350
            case 'html5':
1351
                /*
1352
                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
1353
                 *
1354
                 * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
1355
                 */
1356
                return (bool) preg_match(
1357
                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
1358
                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
1359
                    $address
1360
                );
1361
            case 'php':
1362
            default:
1363
                return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
1364
        }
1365
    }
1366
1367
    /**
1368
     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
1369
     * `intl` and `mbstring` PHP extensions.
1370
     *
1371
     * @return bool `true` if required functions for IDN support are present
1372
     */
1373
    public static function idnSupported()
1374
    {
1375
        return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
1376
    }
1377
1378
    /**
1379
     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
1380
     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
1381
     * This function silently returns unmodified address if:
1382
     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
1383
     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
1384
     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).
1385
     *
1386
     * @see PHPMailer::$CharSet
1387
     *
1388
     * @param string $address The email address to convert
1389
     *
1390
     * @return string The encoded address in ASCII form
1391
     */
1392
    public function punyencodeAddress($address)
1393
    {
1394
        // Verify we have required functions, CharSet, and at-sign.
1395
        $pos = strrpos($address, '@');
1396
        if (!empty($this->CharSet) &&
1397
            false !== $pos &&
1398
            static::idnSupported()
1399
        ) {
1400
            $domain = substr($address, ++$pos);
1401
            // Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
1402
            if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
1403
                $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet);
1404
                //Ignore IDE complaints about this line - method signature changed in PHP 5.4
1405
                $errorcode = 0;
1406
                if (defined('INTL_IDNA_VARIANT_UTS46')) {
1407
                    $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46);
1408
                } elseif (defined('INTL_IDNA_VARIANT_2003')) {
1409
                    $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_2003);
1410
                } else {
1411
                    $punycode = idn_to_ascii($domain, $errorcode);
1412
                }
1413
                if (false !== $punycode) {
1414
                    return substr($address, 0, $pos) . $punycode;
1415
                }
1416
            }
1417
        }
1418
1419
        return $address;
1420
    }
1421
1422
    /**
1423
     * Create a message and send it.
1424
     * Uses the sending method specified by $Mailer.
1425
     *
1426
     * @throws Exception
1427
     *
1428
     * @return bool false on error - See the ErrorInfo property for details of the error
1429
     */
1430
    public function send()
1431
    {
1432
        try {
1433
            if (!$this->preSend()) {
1434
                return false;
1435
            }
1436
1437
            return $this->postSend();
1438
        } catch (Exception $exc) {
1439
            $this->mailHeader = '';
1440
            $this->setError($exc->getMessage());
1441
            if ($this->exceptions) {
1442
                throw $exc;
1443
            }
1444
1445
            return false;
1446
        }
1447
    }
1448
1449
    /**
1450
     * Prepare a message for sending.
1451
     *
1452
     * @throws Exception
1453
     *
1454
     * @return bool
1455
     */
1456
    public function preSend()
1457
    {
1458
        if ('smtp' === $this->Mailer
1459
            || ('mail' === $this->Mailer && stripos(PHP_OS, 'WIN') === 0)
1460
        ) {
1461
            //SMTP mandates RFC-compliant line endings
1462
            //and it's also used with mail() on Windows
1463
            static::setLE(self::CRLF);
1464
        } else {
1465
            //Maintain backward compatibility with legacy Linux command line mailers
1466
            static::setLE(PHP_EOL);
1467
        }
1468
        //Check for buggy PHP versions that add a header with an incorrect line break
1469
        if ('mail' === $this->Mailer
1470
            && ((PHP_VERSION_ID >= 70000 && PHP_VERSION_ID < 70017)
1471
                || (PHP_VERSION_ID >= 70100 && PHP_VERSION_ID < 70103))
1472
            && ini_get('mail.add_x_header') === '1'
1473
            && stripos(PHP_OS, 'WIN') === 0
1474
        ) {
1475
            trigger_error(
1476
                'Your version of PHP is affected by a bug that may result in corrupted messages.' .
1477
                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
1478
                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
1479
                E_USER_WARNING
1480
            );
1481
        }
1482
1483
        try {
1484
            $this->error_count = 0; // Reset errors
1485
            $this->mailHeader = '';
1486
1487
            // Dequeue recipient and Reply-To addresses with IDN
1488
            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
1489
                $params[1] = $this->punyencodeAddress($params[1]);
1490
                call_user_func_array([$this, 'addAnAddress'], $params);
1491
            }
1492
            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
1493
                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
1494
            }
1495
1496
            // Validate From, Sender, and ConfirmReadingTo addresses
1497
            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
1498
                $this->$address_kind = trim($this->$address_kind);
1499
                if (empty($this->$address_kind)) {
1500
                    continue;
1501
                }
1502
                $this->$address_kind = $this->punyencodeAddress($this->$address_kind);
1503
                if (!static::validateAddress($this->$address_kind)) {
1504
                    $error_message = sprintf(
1505
                        '%s (%s): %s',
1506
                        $this->lang('invalid_address'),
1507
                        $address_kind,
1508
                        $this->$address_kind
1509
                    );
1510
                    $this->setError($error_message);
1511
                    $this->edebug($error_message);
1512
                    if ($this->exceptions) {
1513
                        throw new Exception($error_message);
1514
                    }
1515
1516
                    return false;
1517
                }
1518
            }
1519
1520
            // Set whether the message is multipart/alternative
1521
            if ($this->alternativeExists()) {
1522
                $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
1523
            }
1524
1525
            $this->setMessageType();
1526
            // Refuse to send an empty message unless we are specifically allowing it
1527
            if (!$this->AllowEmpty && empty($this->Body)) {
1528
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
1529
            }
1530
1531
            //Trim subject consistently
1532
            $this->Subject = trim($this->Subject);
1533
            // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
1534
            $this->MIMEHeader = '';
1535
            $this->MIMEBody = $this->createBody();
1536
            // createBody may have added some headers, so retain them
1537
            $tempheaders = $this->MIMEHeader;
1538
            $this->MIMEHeader = $this->createHeader();
1539
            $this->MIMEHeader .= $tempheaders;
1540
1541
            // To capture the complete message when using mail(), create
1542
            // an extra header list which createHeader() doesn't fold in
1543
            if ('mail' === $this->Mailer) {
1544
                if (count($this->to) > 0) {
1545
                    $this->mailHeader .= $this->addrAppend('To', $this->to);
1546
                } else {
1547
                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
1548
                }
1549
                $this->mailHeader .= $this->headerLine(
1550
                    'Subject',
1551
                    $this->encodeHeader($this->secureHeader($this->Subject))
1552
                );
1553
            }
1554
1555
            // Sign with DKIM if enabled
1556
            if (!empty($this->DKIM_domain)
1557
                && !empty($this->DKIM_selector)
1558
                && (!empty($this->DKIM_private_string)
1559
                    || (!empty($this->DKIM_private)
1560
                        && static::isPermittedPath($this->DKIM_private)
1561
                        && file_exists($this->DKIM_private)
1562
                    )
1563
                )
1564
            ) {
1565
                $header_dkim = $this->DKIM_Add(
1566
                    $this->MIMEHeader . $this->mailHeader,
1567
                    $this->encodeHeader($this->secureHeader($this->Subject)),
1568
                    $this->MIMEBody
1569
                );
1570
                $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .
1571
                    static::normalizeBreaks($header_dkim) . static::$LE;
1572
            }
1573
1574
            return true;
1575
        } catch (Exception $exc) {
1576
            $this->setError($exc->getMessage());
1577
            if ($this->exceptions) {
1578
                throw $exc;
1579
            }
1580
1581
            return false;
1582
        }
1583
    }
1584
1585
    /**
1586
     * Actually send a message via the selected mechanism.
1587
     *
1588
     * @throws Exception
1589
     *
1590
     * @return bool
1591
     */
1592
    public function postSend()
1593
    {
1594
        try {
1595
            // Choose the mailer and send through it
1596
            switch ($this->Mailer) {
1597
                case 'sendmail':
1598
                case 'qmail':
1599
                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
1600
                case 'smtp':
1601
                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
1602
                case 'mail':
1603
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1604
                default:
1605
                    $sendMethod = $this->Mailer . 'Send';
1606
                    if (method_exists($this, $sendMethod)) {
1607
                        return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody);
1608
                    }
1609
1610
                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
1611
            }
1612
        } catch (Exception $exc) {
1613
            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...
1614
              $this->smtp->reset();
1615
            }
1616
            $this->setError($exc->getMessage());
1617
            $this->edebug($exc->getMessage());
1618
            if ($this->exceptions) {
1619
                throw $exc;
1620
            }
1621
        }
1622
1623
        return false;
1624
    }
1625
1626
    /**
1627
     * Send mail using the $Sendmail program.
1628
     *
1629
     * @see PHPMailer::$Sendmail
1630
     *
1631
     * @param string $header The message headers
1632
     * @param string $body   The message body
1633
     *
1634
     * @throws Exception
1635
     *
1636
     * @return bool
1637
     */
1638
    protected function sendmailSend($header, $body)
1639
    {
1640
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1641
1642
        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1643
        if (!empty($this->Sender) && self::isShellSafe($this->Sender)) {
1644
            if ('qmail' === $this->Mailer) {
1645
                $sendmailFmt = '%s -f%s';
1646
            } else {
1647
                $sendmailFmt = '%s -oi -f%s -t';
1648
            }
1649
        } elseif ('qmail' === $this->Mailer) {
1650
            $sendmailFmt = '%s';
1651
        } else {
1652
            $sendmailFmt = '%s -oi -t';
1653
        }
1654
1655
        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
1656
1657
        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...
1658
            foreach ($this->SingleToArray as $toAddr) {
1659
                $mail = @popen($sendmail, 'w');
1660
                if (!$mail) {
1661
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1662
                }
1663
                fwrite($mail, 'To: ' . $toAddr . "\n");
1664
                fwrite($mail, $header);
1665
                fwrite($mail, $body);
1666
                $result = pclose($mail);
1667
                $this->doCallback(
1668
                    ($result === 0),
1669
                    [$toAddr],
1670
                    $this->cc,
1671
                    $this->bcc,
1672
                    $this->Subject,
1673
                    $body,
1674
                    $this->From,
1675
                    []
1676
                );
1677
                if (0 !== $result) {
1678
                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1679
                }
1680
            }
1681
        } else {
1682
            $mail = @popen($sendmail, 'w');
1683
            if (!$mail) {
1684
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1685
            }
1686
            fwrite($mail, $header);
1687
            fwrite($mail, $body);
1688
            $result = pclose($mail);
1689
            $this->doCallback(
1690
                ($result === 0),
1691
                $this->to,
1692
                $this->cc,
1693
                $this->bcc,
1694
                $this->Subject,
1695
                $body,
1696
                $this->From,
1697
                []
1698
            );
1699
            if (0 !== $result) {
1700
                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
1701
            }
1702
        }
1703
1704
        return true;
1705
    }
1706
1707
    /**
1708
     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
1709
     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
1710
     *
1711
     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
1712
     *
1713
     * @param string $string The string to be validated
1714
     *
1715
     * @return bool
1716
     */
1717
    protected static function isShellSafe($string)
1718
    {
1719
        // Future-proof
1720
        if (escapeshellcmd($string) !== $string
1721
            || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
1722
        ) {
1723
            return false;
1724
        }
1725
1726
        $length = strlen($string);
1727
1728
        for ($i = 0; $i < $length; ++$i) {
1729
            $c = $string[$i];
1730
1731
            // All other characters have a special meaning in at least one common shell, including = and +.
1732
            // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
1733
            // Note that this does permit non-Latin alphanumeric characters based on the current locale.
1734
            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
1735
                return false;
1736
            }
1737
        }
1738
1739
        return true;
1740
    }
1741
1742
    /**
1743
     * Check whether a file path is of a permitted type.
1744
     * Used to reject URLs and phar files from functions that access local file paths,
1745
     * such as addAttachment.
1746
     *
1747
     * @param string $path A relative or absolute path to a file
1748
     *
1749
     * @return bool
1750
     */
1751
    protected static function isPermittedPath($path)
1752
    {
1753
        return !preg_match('#^[a-z]+://#i', $path);
1754
    }
1755
1756
    /**
1757
     * Check whether a file path is safe, accessible, and readable.
1758
     *
1759
     * @param string $path A relative or absolute path to a file
1760
     *
1761
     * @return bool
1762
     */
1763
    protected static function fileIsAccessible($path)
1764
    {
1765
        $readable = file_exists($path);
1766
        //If not a UNC path (expected to start with \\), check read permission, see #2069
1767
        if (strpos($path, '\\\\') !== 0) {
1768
            $readable = $readable && is_readable($path);
1769
        }
1770
        return static::isPermittedPath($path) && $readable;
1771
    }
1772
1773
    /**
1774
     * Send mail using the PHP mail() function.
1775
     *
1776
     * @see http://www.php.net/manual/en/book.mail.php
1777
     *
1778
     * @param string $header The message headers
1779
     * @param string $body   The message body
1780
     *
1781
     * @throws Exception
1782
     *
1783
     * @return bool
1784
     */
1785
    protected function mailSend($header, $body)
1786
    {
1787
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1788
1789
        $toArr = [];
1790
        foreach ($this->to as $toaddr) {
1791
            $toArr[] = $this->addrFormat($toaddr);
1792
        }
1793
        $to = implode(', ', $toArr);
1794
1795
        $params = null;
1796
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
1797
        //A space after `-f` is optional, but there is a long history of its presence
1798
        //causing problems, so we don't use one
1799
        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
1800
        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
1801
        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
1802
        //Example problem: https://www.drupal.org/node/1057954
1803
        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
1804
        if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
1805
            $params = sprintf('-f%s', $this->Sender);
1806
        }
1807
        if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
1808
            $old_from = ini_get('sendmail_from');
1809
            ini_set('sendmail_from', $this->Sender);
1810
        }
1811
        $result = false;
1812
        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...
1813
            foreach ($toArr as $toAddr) {
1814
                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
1815
                $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
1816
            }
1817
        } else {
1818
            $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
1819
            $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
1820
        }
1821
        if (isset($old_from)) {
1822
            ini_set('sendmail_from', $old_from);
1823
        }
1824
        if (!$result) {
1825
            throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
1826
        }
1827
1828
        return true;
1829
    }
1830
1831
    /**
1832
     * Get an instance to use for SMTP operations.
1833
     * Override this function to load your own SMTP implementation,
1834
     * or set one with setSMTPInstance.
1835
     *
1836
     * @return SMTP
1837
     */
1838
    public function getSMTPInstance()
1839
    {
1840
        if (!is_object($this->smtp)) {
1841
            $this->smtp = new SMTP();
1842
        }
1843
1844
        return $this->smtp;
1845
    }
1846
1847
    /**
1848
     * Provide an instance to use for SMTP operations.
1849
     *
1850
     * @return SMTP
1851
     */
1852
    public function setSMTPInstance(SMTP $smtp)
1853
    {
1854
        $this->smtp = $smtp;
1855
1856
        return $this->smtp;
1857
    }
1858
1859
    /**
1860
     * Send mail via SMTP.
1861
     * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
1862
     *
1863
     * @see PHPMailer::setSMTPInstance() to use a different class.
1864
     *
1865
     * @uses \PHPMailer\PHPMailer\SMTP
1866
     *
1867
     * @param string $header The message headers
1868
     * @param string $body   The message body
1869
     *
1870
     * @throws Exception
1871
     *
1872
     * @return bool
1873
     */
1874
    protected function smtpSend($header, $body)
1875
    {
1876
        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
1877
        $bad_rcpt = [];
1878
        if (!$this->smtpConnect($this->SMTPOptions)) {
1879
            throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
1880
        }
1881
        //Sender already validated in preSend()
1882
        if ('' === $this->Sender) {
1883
            $smtp_from = $this->From;
1884
        } else {
1885
            $smtp_from = $this->Sender;
1886
        }
1887
        if (!$this->smtp->mail($smtp_from)) {
1888
            $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
1889
            throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
1890
        }
1891
1892
        $callbacks = [];
1893
        // Attempt to send to all recipients
1894
        foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
1895
            foreach ($togroup as $to) {
1896
                if (!$this->smtp->recipient($to[0], $this->dsn)) {
1897
                    $error = $this->smtp->getError();
1898
                    $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
1899
                    $isSent = false;
1900
                } else {
1901
                    $isSent = true;
1902
                }
1903
1904
                $callbacks[] = ['issent'=>$isSent, 'to'=>$to[0]];
1905
            }
1906
        }
1907
1908
        // Only send the DATA command if we have viable recipients
1909
        if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
1910
            throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
1911
        }
1912
1913
        $smtp_transaction_id = $this->smtp->getLastTransactionID();
1914
1915
        if ($this->SMTPKeepAlive) {
1916
            $this->smtp->reset();
1917
        } else {
1918
            $this->smtp->quit();
1919
            $this->smtp->close();
1920
        }
1921
1922
        foreach ($callbacks as $cb) {
1923
            $this->doCallback(
1924
                $cb['issent'],
1925
                [$cb['to']],
1926
                [],
1927
                [],
1928
                $this->Subject,
1929
                $body,
1930
                $this->From,
1931
                ['smtp_transaction_id' => $smtp_transaction_id]
1932
            );
1933
        }
1934
1935
        //Create error message for any bad addresses
1936
        if (count($bad_rcpt) > 0) {
1937
            $errstr = '';
1938
            foreach ($bad_rcpt as $bad) {
1939
                $errstr .= $bad['to'] . ': ' . $bad['error'];
1940
            }
1941
            throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE);
1942
        }
1943
1944
        return true;
1945
    }
1946
1947
    /**
1948
     * Initiate a connection to an SMTP server.
1949
     * Returns false if the operation failed.
1950
     *
1951
     * @param array $options An array of options compatible with stream_context_create()
1952
     *
1953
     * @throws Exception
1954
     *
1955
     * @uses \PHPMailer\PHPMailer\SMTP
1956
     *
1957
     * @return bool
1958
     */
1959
    public function smtpConnect($options = null)
1960
    {
1961
        if (null === $this->smtp) {
1962
            $this->smtp = $this->getSMTPInstance();
1963
        }
1964
1965
        //If no options are provided, use whatever is set in the instance
1966
        if (null === $options) {
1967
            $options = $this->SMTPOptions;
1968
        }
1969
1970
        // Already connected?
1971
        if ($this->smtp->connected()) {
1972
            return true;
1973
        }
1974
1975
        $this->smtp->setTimeout($this->Timeout);
1976
        $this->smtp->setDebugLevel($this->SMTPDebug);
1977
        $this->smtp->setDebugOutput($this->Debugoutput);
1978
        $this->smtp->setVerp($this->do_verp);
1979
        $hosts = explode(';', $this->Host);
1980
        $lastexception = null;
1981
1982
        foreach ($hosts as $hostentry) {
1983
            $hostinfo = [];
1984
            if (!preg_match(
1985
                '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
1986
                trim($hostentry),
1987
                $hostinfo
1988
            )) {
1989
                $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
1990
                // Not a valid host entry
1991
                continue;
1992
            }
1993
            // $hostinfo[1]: optional ssl or tls prefix
1994
            // $hostinfo[2]: the hostname
1995
            // $hostinfo[3]: optional port number
1996
            // The host string prefix can temporarily override the current setting for SMTPSecure
1997
            // If it's not specified, the default value is used
1998
1999
            //Check the host name is a valid name or IP address before trying to use it
2000
            if (!static::isValidHost($hostinfo[2])) {
2001
                $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
2002
                continue;
2003
            }
2004
            $prefix = '';
2005
            $secure = $this->SMTPSecure;
2006
            $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
2007
            if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
2008
                $prefix = 'ssl://';
2009
                $tls = false; // Can't have SSL and TLS at the same time
2010
                $secure = static::ENCRYPTION_SMTPS;
2011
            } elseif ('tls' === $hostinfo[1]) {
2012
                $tls = true;
2013
                // tls doesn't use a prefix
2014
                $secure = static::ENCRYPTION_STARTTLS;
2015
            }
2016
            //Do we need the OpenSSL extension?
2017
            $sslext = defined('OPENSSL_ALGO_SHA256');
2018
            if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
2019
                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
2020
                if (!$sslext) {
2021
                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
2022
                }
2023
            }
2024
            $host = $hostinfo[2];
2025
            $port = $this->Port;
2026
            if (
2027
                array_key_exists(3, $hostinfo) &&
2028
                is_numeric($hostinfo[3]) &&
2029
                $hostinfo[3] > 0 &&
2030
                $hostinfo[3] < 65536
2031
            ) {
2032
                $port = (int) $hostinfo[3];
2033
            }
2034
            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
2035
                try {
2036
                    if ($this->Helo) {
2037
                        $hello = $this->Helo;
2038
                    } else {
2039
                        $hello = $this->serverHostname();
2040
                    }
2041
                    $this->smtp->hello($hello);
2042
                    //Automatically enable TLS encryption if:
2043
                    // * it's not disabled
2044
                    // * we have openssl extension
2045
                    // * we are not already using SSL
2046
                    // * the server offers STARTTLS
2047
                    if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) {
2048
                        $tls = true;
2049
                    }
2050
                    if ($tls) {
2051
                        if (!$this->smtp->startTLS()) {
2052
                            throw new Exception($this->lang('connect_host'));
2053
                        }
2054
                        // We must resend EHLO after TLS negotiation
2055
                        $this->smtp->hello($hello);
2056
                    }
2057
                    if ($this->SMTPAuth && !$this->smtp->authenticate(
2058
                        $this->Username,
2059
                        $this->Password,
2060
                        $this->AuthType,
2061
                        $this->oauth
2062
                    )) {
2063
                        throw new Exception($this->lang('authenticate'));
2064
                    }
2065
2066
                    return true;
2067
                } catch (Exception $exc) {
2068
                    $lastexception = $exc;
2069
                    $this->edebug($exc->getMessage());
2070
                    // We must have connected, but then failed TLS or Auth, so close connection nicely
2071
                    $this->smtp->quit();
2072
                }
2073
            }
2074
        }
2075
        // If we get here, all connection attempts have failed, so close connection hard
2076
        $this->smtp->close();
2077
        // As we've caught all exceptions, just report whatever the last one was
2078
        if ($this->exceptions && null !== $lastexception) {
2079
            throw $lastexception;
2080
        }
2081
2082
        return false;
2083
    }
2084
2085
    /**
2086
     * Close the active SMTP session if one exists.
2087
     */
2088
    public function smtpClose()
2089
    {
2090
        if ((null !== $this->smtp) && $this->smtp->connected()) {
2091
            $this->smtp->quit();
2092
            $this->smtp->close();
2093
        }
2094
    }
2095
2096
    /**
2097
     * Set the language for error messages.
2098
     * Returns false if it cannot load the language file.
2099
     * The default language is English.
2100
     *
2101
     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
2102
     * @param string $lang_path Path to the language file directory, with trailing separator (slash)
2103
     *
2104
     * @return bool
2105
     */
2106
    public function setLanguage($langcode = 'en', $lang_path = '')
2107
    {
2108
        // Backwards compatibility for renamed language codes
2109
        $renamed_langcodes = [
2110
            'br' => 'pt_br',
2111
            'cz' => 'cs',
2112
            'dk' => 'da',
2113
            'no' => 'nb',
2114
            'se' => 'sv',
2115
            'rs' => 'sr',
2116
            'tg' => 'tl',
2117
            'am' => 'hy',
2118
        ];
2119
2120
        if (isset($renamed_langcodes[$langcode])) {
2121
            $langcode = $renamed_langcodes[$langcode];
2122
        }
2123
2124
        // Define full set of translatable strings in English
2125
        $PHPMAILER_LANG = [
2126
            'authenticate' => 'SMTP Error: Could not authenticate.',
2127
            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
2128
            'data_not_accepted' => 'SMTP Error: data not accepted.',
2129
            'empty_message' => 'Message body empty',
2130
            'encoding' => 'Unknown encoding: ',
2131
            'execute' => 'Could not execute: ',
2132
            'file_access' => 'Could not access file: ',
2133
            'file_open' => 'File Error: Could not open file: ',
2134
            'from_failed' => 'The following From address failed: ',
2135
            'instantiate' => 'Could not instantiate mail function.',
2136
            'invalid_address' => 'Invalid address: ',
2137
            'invalid_hostentry' => 'Invalid hostentry: ',
2138
            'invalid_host' => 'Invalid host: ',
2139
            'mailer_not_supported' => ' mailer is not supported.',
2140
            'provide_address' => 'You must provide at least one recipient email address.',
2141
            'recipients_failed' => 'SMTP Error: The following recipients failed: ',
2142
            'signing' => 'Signing Error: ',
2143
            'smtp_connect_failed' => 'SMTP connect() failed.',
2144
            'smtp_error' => 'SMTP server error: ',
2145
            'variable_set' => 'Cannot set or reset variable: ',
2146
            'extension_missing' => 'Extension missing: ',
2147
        ];
2148
        if (empty($lang_path)) {
2149
            // Calculate an absolute path so it can work if CWD is not here
2150
            $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
2151
        }
2152
        //Validate $langcode
2153
        if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) {
2154
            $langcode = 'en';
2155
        }
2156
        $foundlang = true;
2157
        $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
2158
        // There is no English translation file
2159
        if ('en' !== $langcode) {
2160
            // Make sure language file path is readable
2161
            if (!static::fileIsAccessible($lang_file)) {
2162
                $foundlang = false;
2163
            } else {
2164
                // Overwrite language-specific strings.
2165
                // This way we'll never have missing translation keys.
2166
                $foundlang = include $lang_file;
2167
            }
2168
        }
2169
        $this->language = $PHPMAILER_LANG;
2170
2171
        return (bool) $foundlang; // Returns false if language not found
2172
    }
2173
2174
    /**
2175
     * Get the array of strings for the current language.
2176
     *
2177
     * @return array
2178
     */
2179
    public function getTranslations()
2180
    {
2181
        return $this->language;
2182
    }
2183
2184
    /**
2185
     * Create recipient headers.
2186
     *
2187
     * @param string $type
2188
     * @param array  $addr An array of recipients,
2189
     *                     where each recipient is a 2-element indexed array with element 0 containing an address
2190
     *                     and element 1 containing a name, like:
2191
     *                     [['[email protected]', 'Joe User'], ['[email protected]', 'Zoe User']]
2192
     *
2193
     * @return string
2194
     */
2195
    public function addrAppend($type, $addr)
2196
    {
2197
        $addresses = [];
2198
        foreach ($addr as $address) {
2199
            $addresses[] = $this->addrFormat($address);
2200
        }
2201
2202
        return $type . ': ' . implode(', ', $addresses) . static::$LE;
2203
    }
2204
2205
    /**
2206
     * Format an address for use in a message header.
2207
     *
2208
     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
2209
     *                    ['[email protected]', 'Joe User']
2210
     *
2211
     * @return string
2212
     */
2213
    public function addrFormat($addr)
2214
    {
2215
        if (empty($addr[1])) { // No name provided
2216
            return $this->secureHeader($addr[0]);
2217
        }
2218
2219
        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
2220
            ' <' . $this->secureHeader($addr[0]) . '>';
2221
    }
2222
2223
    /**
2224
     * Word-wrap message.
2225
     * For use with mailers that do not automatically perform wrapping
2226
     * and for quoted-printable encoded messages.
2227
     * Original written by philippe.
2228
     *
2229
     * @param string $message The message to wrap
2230
     * @param int    $length  The line length to wrap to
2231
     * @param bool   $qp_mode Whether to run in Quoted-Printable mode
2232
     *
2233
     * @return string
2234
     */
2235
    public function wrapText($message, $length, $qp_mode = false)
2236
    {
2237
        if ($qp_mode) {
2238
            $soft_break = sprintf(' =%s', static::$LE);
2239
        } else {
2240
            $soft_break = static::$LE;
2241
        }
2242
        // If utf-8 encoding is used, we will need to make sure we don't
2243
        // split multibyte characters when we wrap
2244
        $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
2245
        $lelen = strlen(static::$LE);
2246
        $crlflen = strlen(static::$LE);
2247
2248
        $message = static::normalizeBreaks($message);
2249
        //Remove a trailing line break
2250
        if (substr($message, -$lelen) === static::$LE) {
2251
            $message = substr($message, 0, -$lelen);
2252
        }
2253
2254
        //Split message into lines
2255
        $lines = explode(static::$LE, $message);
2256
        //Message will be rebuilt in here
2257
        $message = '';
2258
        foreach ($lines as $line) {
2259
            $words = explode(' ', $line);
2260
            $buf = '';
2261
            $firstword = true;
2262
            foreach ($words as $word) {
2263
                if ($qp_mode && (strlen($word) > $length)) {
2264
                    $space_left = $length - strlen($buf) - $crlflen;
2265
                    if (!$firstword) {
2266
                        if ($space_left > 20) {
2267
                            $len = $space_left;
2268
                            if ($is_utf8) {
2269
                                $len = $this->utf8CharBoundary($word, $len);
2270
                            } elseif ('=' === substr($word, $len - 1, 1)) {
2271
                                --$len;
2272
                            } elseif ('=' === substr($word, $len - 2, 1)) {
2273
                                $len -= 2;
2274
                            }
2275
                            $part = substr($word, 0, $len);
2276
                            $word = substr($word, $len);
2277
                            $buf .= ' ' . $part;
2278
                            $message .= $buf . sprintf('=%s', static::$LE);
2279
                        } else {
2280
                            $message .= $buf . $soft_break;
2281
                        }
2282
                        $buf = '';
2283
                    }
2284
                    while ($word !== '') {
2285
                        if ($length <= 0) {
2286
                            break;
2287
                        }
2288
                        $len = $length;
2289
                        if ($is_utf8) {
2290
                            $len = $this->utf8CharBoundary($word, $len);
2291
                        } elseif ('=' === substr($word, $len - 1, 1)) {
2292
                            --$len;
2293
                        } elseif ('=' === substr($word, $len - 2, 1)) {
2294
                            $len -= 2;
2295
                        }
2296
                        $part = substr($word, 0, $len);
2297
                        $word = (string) substr($word, $len);
2298
2299
                        if ($word !== '') {
2300
                            $message .= $part . sprintf('=%s', static::$LE);
2301
                        } else {
2302
                            $buf = $part;
2303
                        }
2304
                    }
2305
                } else {
2306
                    $buf_o = $buf;
2307
                    if (!$firstword) {
2308
                        $buf .= ' ';
2309
                    }
2310
                    $buf .= $word;
2311
2312
                    if ('' !== $buf_o && strlen($buf) > $length) {
2313
                        $message .= $buf_o . $soft_break;
2314
                        $buf = $word;
2315
                    }
2316
                }
2317
                $firstword = false;
2318
            }
2319
            $message .= $buf . static::$LE;
2320
        }
2321
2322
        return $message;
2323
    }
2324
2325
    /**
2326
     * Find the last character boundary prior to $maxLength in a utf-8
2327
     * quoted-printable encoded string.
2328
     * Original written by Colin Brown.
2329
     *
2330
     * @param string $encodedText utf-8 QP text
2331
     * @param int    $maxLength   Find the last character boundary prior to this length
2332
     *
2333
     * @return int
2334
     */
2335
    public function utf8CharBoundary($encodedText, $maxLength)
2336
    {
2337
        $foundSplitPos = false;
2338
        $lookBack = 3;
2339
        while (!$foundSplitPos) {
2340
            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
2341
            $encodedCharPos = strpos($lastChunk, '=');
2342
            if (false !== $encodedCharPos) {
2343
                // Found start of encoded character byte within $lookBack block.
2344
                // Check the encoded byte value (the 2 chars after the '=')
2345
                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
2346
                $dec = hexdec($hex);
2347
                if ($dec < 128) {
2348
                    // Single byte character.
2349
                    // If the encoded char was found at pos 0, it will fit
2350
                    // otherwise reduce maxLength to start of the encoded char
2351
                    if ($encodedCharPos > 0) {
2352
                        $maxLength -= $lookBack - $encodedCharPos;
2353
                    }
2354
                    $foundSplitPos = true;
2355
                } elseif ($dec >= 192) {
2356
                    // First byte of a multi byte character
2357
                    // Reduce maxLength to split at start of character
2358
                    $maxLength -= $lookBack - $encodedCharPos;
2359
                    $foundSplitPos = true;
2360
                } elseif ($dec < 192) {
2361
                    // Middle byte of a multi byte character, look further back
2362
                    $lookBack += 3;
2363
                }
2364
            } else {
2365
                // No encoded character found
2366
                $foundSplitPos = true;
2367
            }
2368
        }
2369
2370
        return $maxLength;
2371
    }
2372
2373
    /**
2374
     * Apply word wrapping to the message body.
2375
     * Wraps the message body to the number of chars set in the WordWrap property.
2376
     * You should only do this to plain-text bodies as wrapping HTML tags may break them.
2377
     * This is called automatically by createBody(), so you don't need to call it yourself.
2378
     */
2379
    public function setWordWrap()
2380
    {
2381
        if ($this->WordWrap < 1) {
2382
            return;
2383
        }
2384
2385
        switch ($this->message_type) {
2386
            case 'alt':
2387
            case 'alt_inline':
2388
            case 'alt_attach':
2389
            case 'alt_inline_attach':
2390
                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
2391
                break;
2392
            default:
2393
                $this->Body = $this->wrapText($this->Body, $this->WordWrap);
2394
                break;
2395
        }
2396
    }
2397
2398
    /**
2399
     * Assemble message headers.
2400
     *
2401
     * @return string The assembled headers
2402
     */
2403
    public function createHeader()
2404
    {
2405
        $result = '';
2406
2407
        $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
2408
2409
        // The To header is created automatically by mail(), so needs to be omitted here
2410
        if ('mail' !== $this->Mailer) {
2411
            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...
2412
                foreach ($this->to as $toaddr) {
2413
                    $this->SingleToArray[] = $this->addrFormat($toaddr);
2414
                }
2415
            } elseif (count($this->to) > 0) {
2416
                $result .= $this->addrAppend('To', $this->to);
2417
            } elseif (count($this->cc) === 0) {
2418
                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
2419
            }
2420
        }
2421
        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
2422
2423
        // sendmail and mail() extract Cc from the header before sending
2424
        if (count($this->cc) > 0) {
2425
            $result .= $this->addrAppend('Cc', $this->cc);
2426
        }
2427
2428
        // sendmail and mail() extract Bcc from the header before sending
2429
        if ((
2430
                'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
2431
            )
2432
            && count($this->bcc) > 0
2433
        ) {
2434
            $result .= $this->addrAppend('Bcc', $this->bcc);
2435
        }
2436
2437
        if (count($this->ReplyTo) > 0) {
2438
            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
2439
        }
2440
2441
        // mail() sets the subject itself
2442
        if ('mail' !== $this->Mailer) {
2443
            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
2444
        }
2445
2446
        // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
2447
        // https://tools.ietf.org/html/rfc5322#section-3.6.4
2448
        if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageID)) {
2449
            $this->lastMessageID = $this->MessageID;
2450
        } else {
2451
            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
2452
        }
2453
        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
2454
        if (null !== $this->Priority) {
2455
            $result .= $this->headerLine('X-Priority', $this->Priority);
2456
        }
2457
        if ('' === $this->XMailer) {
2458
            $result .= $this->headerLine(
2459
                'X-Mailer',
2460
                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
2461
            );
2462
        } else {
2463
            $myXmailer = trim($this->XMailer);
2464
            if ($myXmailer) {
2465
                $result .= $this->headerLine('X-Mailer', $myXmailer);
2466
            }
2467
        }
2468
2469
        if ('' !== $this->ConfirmReadingTo) {
2470
            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
2471
        }
2472
2473
        // Add custom headers
2474
        foreach ($this->CustomHeader as $header) {
2475
            $result .= $this->headerLine(
2476
                trim($header[0]),
2477
                $this->encodeHeader(trim($header[1]))
2478
            );
2479
        }
2480
        if (!$this->sign_key_file) {
2481
            $result .= $this->headerLine('MIME-Version', '1.0');
2482
            $result .= $this->getMailMIME();
2483
        }
2484
2485
        return $result;
2486
    }
2487
2488
    /**
2489
     * Get the message MIME type headers.
2490
     *
2491
     * @return string
2492
     */
2493
    public function getMailMIME()
2494
    {
2495
        $result = '';
2496
        $ismultipart = true;
2497
        switch ($this->message_type) {
2498
            case 'inline':
2499
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2500
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2501
                break;
2502
            case 'attach':
2503
            case 'inline_attach':
2504
            case 'alt_attach':
2505
            case 'alt_inline_attach':
2506
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';');
2507
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2508
                break;
2509
            case 'alt':
2510
            case 'alt_inline':
2511
                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2512
                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
2513
                break;
2514
            default:
2515
                // Catches case 'plain': and case '':
2516
                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
2517
                $ismultipart = false;
2518
                break;
2519
        }
2520
        // RFC1341 part 5 says 7bit is assumed if not specified
2521
        if (static::ENCODING_7BIT !== $this->Encoding) {
2522
            // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
2523
            if ($ismultipart) {
2524
                if (static::ENCODING_8BIT === $this->Encoding) {
2525
                    $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
2526
                }
2527
                // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
2528
            } else {
2529
                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
2530
            }
2531
        }
2532
2533
        if ('mail' !== $this->Mailer) {
2534
//            $result .= static::$LE;
2535
        }
2536
2537
        return $result;
2538
    }
2539
2540
    /**
2541
     * Returns the whole MIME message.
2542
     * Includes complete headers and body.
2543
     * Only valid post preSend().
2544
     *
2545
     * @see PHPMailer::preSend()
2546
     *
2547
     * @return string
2548
     */
2549
    public function getSentMIMEMessage()
2550
    {
2551
        return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) .
2552
            static::$LE . static::$LE . $this->MIMEBody;
2553
    }
2554
2555
    /**
2556
     * Create a unique ID to use for boundaries.
2557
     *
2558
     * @return string
2559
     */
2560
    protected function generateId()
2561
    {
2562
        $len = 32; //32 bytes = 256 bits
2563
        $bytes = '';
2564
        if (function_exists('random_bytes')) {
2565
            try {
2566
                $bytes = random_bytes($len);
2567
            } catch (\Exception $e) {
2568
                //Do nothing
2569
            }
2570
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
2571
            /** @noinspection CryptographicallySecureRandomnessInspection */
2572
            $bytes = openssl_random_pseudo_bytes($len);
2573
        }
2574
        if ($bytes === '') {
2575
            //We failed to produce a proper random string, so make do.
2576
            //Use a hash to force the length to the same as the other methods
2577
            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
2578
        }
2579
2580
        //We don't care about messing up base64 format here, just want a random string
2581
        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
2582
    }
2583
2584
    /**
2585
     * Assemble the message body.
2586
     * Returns an empty string on failure.
2587
     *
2588
     * @throws Exception
2589
     *
2590
     * @return string The assembled message body
2591
     */
2592
    public function createBody()
2593
    {
2594
        $body = '';
2595
        //Create unique IDs and preset boundaries
2596
        $this->uniqueid = $this->generateId();
2597
        $this->boundary[1] = 'b1_' . $this->uniqueid;
2598
        $this->boundary[2] = 'b2_' . $this->uniqueid;
2599
        $this->boundary[3] = 'b3_' . $this->uniqueid;
2600
2601
        if ($this->sign_key_file) {
2602
            $body .= $this->getMailMIME() . static::$LE;
2603
        }
2604
2605
        $this->setWordWrap();
2606
2607
        $bodyEncoding = $this->Encoding;
2608
        $bodyCharSet = $this->CharSet;
2609
        //Can we do a 7-bit downgrade?
2610
        if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {
2611
            $bodyEncoding = static::ENCODING_7BIT;
2612
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2613
            $bodyCharSet = static::CHARSET_ASCII;
2614
        }
2615
        //If lines are too long, and we're not already using an encoding that will shorten them,
2616
        //change to quoted-printable transfer encoding for the body part only
2617
        if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {
2618
            $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2619
        }
2620
2621
        $altBodyEncoding = $this->Encoding;
2622
        $altBodyCharSet = $this->CharSet;
2623
        //Can we do a 7-bit downgrade?
2624
        if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {
2625
            $altBodyEncoding = static::ENCODING_7BIT;
2626
            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
2627
            $altBodyCharSet = 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 alt body part only
2631
        if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {
2632
            $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
2633
        }
2634
        //Use this as a preamble in all multipart message types
2635
        $mimepre = 'This is a multi-part message in MIME format.' . static::$LE . static::$LE;
2636
        switch ($this->message_type) {
2637
            case 'inline':
2638
                $body .= $mimepre;
2639
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2640
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2641
                $body .= static::$LE;
2642
                $body .= $this->attachAll('inline', $this->boundary[1]);
2643
                break;
2644
            case 'attach':
2645
                $body .= $mimepre;
2646
                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
2647
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2648
                $body .= static::$LE;
2649
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2650
                break;
2651
            case 'inline_attach':
2652
                $body .= $mimepre;
2653
                $body .= $this->textLine('--' . $this->boundary[1]);
2654
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2655
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
2656
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2657
                $body .= static::$LE;
2658
                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
2659
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2660
                $body .= static::$LE;
2661
                $body .= $this->attachAll('inline', $this->boundary[2]);
2662
                $body .= static::$LE;
2663
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2664
                break;
2665
            case 'alt':
2666
                $body .= $mimepre;
2667
                $body .= $this->getBoundary(
2668
                    $this->boundary[1],
2669
                    $altBodyCharSet,
2670
                    static::CONTENT_TYPE_PLAINTEXT,
2671
                    $altBodyEncoding
2672
                );
2673
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2674
                $body .= static::$LE;
2675
                $body .= $this->getBoundary(
2676
                    $this->boundary[1],
2677
                    $bodyCharSet,
2678
                    static::CONTENT_TYPE_TEXT_HTML,
2679
                    $bodyEncoding
2680
                );
2681
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2682
                $body .= static::$LE;
2683
                if (!empty($this->Ical)) {
2684
                    $method = static::ICAL_METHOD_REQUEST;
2685
                    foreach (static::$IcalMethods as $imethod) {
2686
                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
2687
                            $method = $imethod;
2688
                            break;
2689
                        }
2690
                    }
2691
                    $body .= $this->getBoundary(
2692
                        $this->boundary[1],
2693
                        '',
2694
                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
2695
                        ''
2696
                    );
2697
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2698
                    $body .= static::$LE;
2699
                }
2700
                $body .= $this->endBoundary($this->boundary[1]);
2701
                break;
2702
            case 'alt_inline':
2703
                $body .= $mimepre;
2704
                $body .= $this->getBoundary(
2705
                    $this->boundary[1],
2706
                    $altBodyCharSet,
2707
                    static::CONTENT_TYPE_PLAINTEXT,
2708
                    $altBodyEncoding
2709
                );
2710
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2711
                $body .= static::$LE;
2712
                $body .= $this->textLine('--' . $this->boundary[1]);
2713
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2714
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
2715
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2716
                $body .= static::$LE;
2717
                $body .= $this->getBoundary(
2718
                    $this->boundary[2],
2719
                    $bodyCharSet,
2720
                    static::CONTENT_TYPE_TEXT_HTML,
2721
                    $bodyEncoding
2722
                );
2723
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2724
                $body .= static::$LE;
2725
                $body .= $this->attachAll('inline', $this->boundary[2]);
2726
                $body .= static::$LE;
2727
                $body .= $this->endBoundary($this->boundary[1]);
2728
                break;
2729
            case 'alt_attach':
2730
                $body .= $mimepre;
2731
                $body .= $this->textLine('--' . $this->boundary[1]);
2732
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2733
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
2734
                $body .= static::$LE;
2735
                $body .= $this->getBoundary(
2736
                    $this->boundary[2],
2737
                    $altBodyCharSet,
2738
                    static::CONTENT_TYPE_PLAINTEXT,
2739
                    $altBodyEncoding
2740
                );
2741
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2742
                $body .= static::$LE;
2743
                $body .= $this->getBoundary(
2744
                    $this->boundary[2],
2745
                    $bodyCharSet,
2746
                    static::CONTENT_TYPE_TEXT_HTML,
2747
                    $bodyEncoding
2748
                );
2749
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2750
                $body .= static::$LE;
2751
                if (!empty($this->Ical)) {
2752
                    $method = static::ICAL_METHOD_REQUEST;
2753
                    foreach (static::$IcalMethods as $imethod) {
2754
                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
2755
                            $method = $imethod;
2756
                            break;
2757
                        }
2758
                    }
2759
                    $body .= $this->getBoundary(
2760
                        $this->boundary[2],
2761
                        '',
2762
                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
2763
                        ''
2764
                    );
2765
                    $body .= $this->encodeString($this->Ical, $this->Encoding);
2766
                }
2767
                $body .= $this->endBoundary($this->boundary[2]);
2768
                $body .= static::$LE;
2769
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2770
                break;
2771
            case 'alt_inline_attach':
2772
                $body .= $mimepre;
2773
                $body .= $this->textLine('--' . $this->boundary[1]);
2774
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
2775
                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
2776
                $body .= static::$LE;
2777
                $body .= $this->getBoundary(
2778
                    $this->boundary[2],
2779
                    $altBodyCharSet,
2780
                    static::CONTENT_TYPE_PLAINTEXT,
2781
                    $altBodyEncoding
2782
                );
2783
                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
2784
                $body .= static::$LE;
2785
                $body .= $this->textLine('--' . $this->boundary[2]);
2786
                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
2787
                $body .= $this->textLine(' boundary="' . $this->boundary[3] . '";');
2788
                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
2789
                $body .= static::$LE;
2790
                $body .= $this->getBoundary(
2791
                    $this->boundary[3],
2792
                    $bodyCharSet,
2793
                    static::CONTENT_TYPE_TEXT_HTML,
2794
                    $bodyEncoding
2795
                );
2796
                $body .= $this->encodeString($this->Body, $bodyEncoding);
2797
                $body .= static::$LE;
2798
                $body .= $this->attachAll('inline', $this->boundary[3]);
2799
                $body .= static::$LE;
2800
                $body .= $this->endBoundary($this->boundary[2]);
2801
                $body .= static::$LE;
2802
                $body .= $this->attachAll('attachment', $this->boundary[1]);
2803
                break;
2804
            default:
2805
                // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
2806
                //Reset the `Encoding` property in case we changed it for line length reasons
2807
                $this->Encoding = $bodyEncoding;
2808
                $body .= $this->encodeString($this->Body, $this->Encoding);
2809
                break;
2810
        }
2811
2812
        if ($this->isError()) {
2813
            $body = '';
2814
            if ($this->exceptions) {
2815
                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
2816
            }
2817
        } elseif ($this->sign_key_file) {
2818
            try {
2819
                if (!defined('PKCS7_TEXT')) {
2820
                    throw new Exception($this->lang('extension_missing') . 'openssl');
2821
                }
2822
2823
                $file = tempnam(sys_get_temp_dir(), 'srcsign');
2824
                $signed = tempnam(sys_get_temp_dir(), 'mailsign');
2825
                file_put_contents($file, $body);
2826
2827
                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
2828
                if (empty($this->sign_extracerts_file)) {
2829
                    $sign = @openssl_pkcs7_sign(
2830
                        $file,
2831
                        $signed,
2832
                        'file://' . realpath($this->sign_cert_file),
2833
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2834
                        []
2835
                    );
2836
                } else {
2837
                    $sign = @openssl_pkcs7_sign(
2838
                        $file,
2839
                        $signed,
2840
                        'file://' . realpath($this->sign_cert_file),
2841
                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
2842
                        [],
2843
                        PKCS7_DETACHED,
2844
                        $this->sign_extracerts_file
2845
                    );
2846
                }
2847
2848
                @unlink($file);
2849
                if ($sign) {
2850
                    $body = file_get_contents($signed);
2851
                    @unlink($signed);
2852
                    //The message returned by openssl contains both headers and body, so need to split them up
2853
                    $parts = explode("\n\n", $body, 2);
2854
                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
2855
                    $body = $parts[1];
2856
                } else {
2857
                    @unlink($signed);
2858
                    throw new Exception($this->lang('signing') . openssl_error_string());
2859
                }
2860
            } catch (Exception $exc) {
2861
                $body = '';
2862
                if ($this->exceptions) {
2863
                    throw $exc;
2864
                }
2865
            }
2866
        }
2867
2868
        return $body;
2869
    }
2870
2871
    /**
2872
     * Return the start of a message boundary.
2873
     *
2874
     * @param string $boundary
2875
     * @param string $charSet
2876
     * @param string $contentType
2877
     * @param string $encoding
2878
     *
2879
     * @return string
2880
     */
2881
    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
2882
    {
2883
        $result = '';
2884
        if ('' === $charSet) {
2885
            $charSet = $this->CharSet;
2886
        }
2887
        if ('' === $contentType) {
2888
            $contentType = $this->ContentType;
2889
        }
2890
        if ('' === $encoding) {
2891
            $encoding = $this->Encoding;
2892
        }
2893
        $result .= $this->textLine('--' . $boundary);
2894
        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
2895
        $result .= static::$LE;
2896
        // RFC1341 part 5 says 7bit is assumed if not specified
2897
        if (static::ENCODING_7BIT !== $encoding) {
2898
            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
2899
        }
2900
        $result .= static::$LE;
2901
2902
        return $result;
2903
    }
2904
2905
    /**
2906
     * Return the end of a message boundary.
2907
     *
2908
     * @param string $boundary
2909
     *
2910
     * @return string
2911
     */
2912
    protected function endBoundary($boundary)
2913
    {
2914
        return static::$LE . '--' . $boundary . '--' . static::$LE;
2915
    }
2916
2917
    /**
2918
     * Set the message type.
2919
     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
2920
     */
2921
    protected function setMessageType()
2922
    {
2923
        $type = [];
2924
        if ($this->alternativeExists()) {
2925
            $type[] = 'alt';
2926
        }
2927
        if ($this->inlineImageExists()) {
2928
            $type[] = 'inline';
2929
        }
2930
        if ($this->attachmentExists()) {
2931
            $type[] = 'attach';
2932
        }
2933
        $this->message_type = implode('_', $type);
2934
        if ('' === $this->message_type) {
2935
            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
2936
            $this->message_type = 'plain';
2937
        }
2938
    }
2939
2940
    /**
2941
     * Format a header line.
2942
     *
2943
     * @param string     $name
2944
     * @param string|int $value
2945
     *
2946
     * @return string
2947
     */
2948
    public function headerLine($name, $value)
2949
    {
2950
        return $name . ': ' . $value . static::$LE;
2951
    }
2952
2953
    /**
2954
     * Return a formatted mail line.
2955
     *
2956
     * @param string $value
2957
     *
2958
     * @return string
2959
     */
2960
    public function textLine($value)
2961
    {
2962
        return $value . static::$LE;
2963
    }
2964
2965
    /**
2966
     * Add an attachment from a path on the filesystem.
2967
     * Never use a user-supplied path to a file!
2968
     * Returns false if the file could not be found or read.
2969
     * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.
2970
     * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
2971
     *
2972
     * @param string $path        Path to the attachment
2973
     * @param string $name        Overrides the attachment name
2974
     * @param string $encoding    File encoding (see $Encoding)
2975
     * @param string $type        MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
2976
     * @param string $disposition Disposition to use
2977
     *
2978
     * @throws Exception
2979
     *
2980
     * @return bool
2981
     */
2982
    public function addAttachment(
2983
        $path,
2984
        $name = '',
2985
        $encoding = self::ENCODING_BASE64,
2986
        $type = '',
2987
        $disposition = 'attachment'
2988
    ) {
2989
        try {
2990
            if (!static::fileIsAccessible($path)) {
2991
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
2992
            }
2993
2994
            // If a MIME type is not specified, try to work it out from the file name
2995
            if ('' === $type) {
2996
                $type = static::filenameToType($path);
2997
            }
2998
2999
            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
3000
            if ('' === $name) {
3001
                $name = $filename;
3002
            }
3003
            if (!$this->validateEncoding($encoding)) {
3004
                throw new Exception($this->lang('encoding') . $encoding);
3005
            }
3006
3007
            $this->attachment[] = [
3008
                0 => $path,
3009
                1 => $filename,
3010
                2 => $name,
3011
                3 => $encoding,
3012
                4 => $type,
3013
                5 => false, // isStringAttachment
3014
                6 => $disposition,
3015
                7 => $name,
3016
            ];
3017
        } catch (Exception $exc) {
3018
            $this->setError($exc->getMessage());
3019
            $this->edebug($exc->getMessage());
3020
            if ($this->exceptions) {
3021
                throw $exc;
3022
            }
3023
3024
            return false;
3025
        }
3026
3027
        return true;
3028
    }
3029
3030
    /**
3031
     * Return the array of attachments.
3032
     *
3033
     * @return array
3034
     */
3035
    public function getAttachments()
3036
    {
3037
        return $this->attachment;
3038
    }
3039
3040
    /**
3041
     * Attach all file, string, and binary attachments to the message.
3042
     * Returns an empty string on failure.
3043
     *
3044
     * @param string $disposition_type
3045
     * @param string $boundary
3046
     *
3047
     * @throws Exception
3048
     *
3049
     * @return string
3050
     */
3051
    protected function attachAll($disposition_type, $boundary)
3052
    {
3053
        // Return text of body
3054
        $mime = [];
3055
        $cidUniq = [];
3056
        $incl = [];
3057
3058
        // Add all attachments
3059
        foreach ($this->attachment as $attachment) {
3060
            // Check if it is a valid disposition_filter
3061
            if ($attachment[6] === $disposition_type) {
3062
                // Check for string attachment
3063
                $string = '';
3064
                $path = '';
3065
                $bString = $attachment[5];
3066
                if ($bString) {
3067
                    $string = $attachment[0];
3068
                } else {
3069
                    $path = $attachment[0];
3070
                }
3071
3072
                $inclhash = hash('sha256', serialize($attachment));
3073
                if (in_array($inclhash, $incl, true)) {
3074
                    continue;
3075
                }
3076
                $incl[] = $inclhash;
3077
                $name = $attachment[2];
3078
                $encoding = $attachment[3];
3079
                $type = $attachment[4];
3080
                $disposition = $attachment[6];
3081
                $cid = $attachment[7];
3082
                if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {
3083
                    continue;
3084
                }
3085
                $cidUniq[$cid] = true;
3086
3087
                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
3088
                //Only include a filename property if we have one
3089
                if (!empty($name)) {
3090
                    $mime[] = sprintf(
3091
                        'Content-Type: %s; name=%s%s',
3092
                        $type,
3093
                        static::quotedString($this->encodeHeader($this->secureHeader($name))),
3094
                        static::$LE
3095
                    );
3096
                } else {
3097
                    $mime[] = sprintf(
3098
                        'Content-Type: %s%s',
3099
                        $type,
3100
                        static::$LE
3101
                    );
3102
                }
3103
                // RFC1341 part 5 says 7bit is assumed if not specified
3104
                if (static::ENCODING_7BIT !== $encoding) {
3105
                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
3106
                }
3107
3108
                //Only set Content-IDs on inline attachments
3109
                if ((string) $cid !== '' && $disposition === 'inline') {
3110
                    $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
3111
                }
3112
3113
                // Allow for bypassing the Content-Disposition header
3114
                if (!empty($disposition)) {
3115
                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
3116
                    if (!empty($encoded_name)) {
3117
                        $mime[] = sprintf(
3118
                            'Content-Disposition: %s; filename=%s%s',
3119
                            $disposition,
3120
                            static::quotedString($encoded_name),
3121
                            static::$LE . static::$LE
3122
                        );
3123
                    } else {
3124
                        $mime[] = sprintf(
3125
                            'Content-Disposition: %s%s',
3126
                            $disposition,
3127
                            static::$LE . static::$LE
3128
                        );
3129
                    }
3130
                } else {
3131
                    $mime[] = static::$LE;
3132
                }
3133
3134
                // Encode as string attachment
3135
                if ($bString) {
3136
                    $mime[] = $this->encodeString($string, $encoding);
3137
                } else {
3138
                    $mime[] = $this->encodeFile($path, $encoding);
3139
                }
3140
                if ($this->isError()) {
3141
                    return '';
3142
                }
3143
                $mime[] = static::$LE;
3144
            }
3145
        }
3146
3147
        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
3148
3149
        return implode('', $mime);
3150
    }
3151
3152
    /**
3153
     * Encode a file attachment in requested format.
3154
     * Returns an empty string on failure.
3155
     *
3156
     * @param string $path     The full path to the file
3157
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3158
     *
3159
     * @return string
3160
     */
3161
    protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
3162
    {
3163
        try {
3164
            if (!static::fileIsAccessible($path)) {
3165
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
3166
            }
3167
            $file_buffer = file_get_contents($path);
3168
            if (false === $file_buffer) {
3169
                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
3170
            }
3171
            $file_buffer = $this->encodeString($file_buffer, $encoding);
3172
3173
            return $file_buffer;
3174
        } catch (Exception $exc) {
3175
            $this->setError($exc->getMessage());
3176
            $this->edebug($exc->getMessage());
3177
            if ($this->exceptions) {
3178
                throw $exc;
3179
            }
3180
3181
            return '';
3182
        }
3183
    }
3184
3185
    /**
3186
     * Encode a string in requested format.
3187
     * Returns an empty string on failure.
3188
     *
3189
     * @param string $str      The text to encode
3190
     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
3191
     *
3192
     * @throws Exception
3193
     *
3194
     * @return string
3195
     */
3196
    public function encodeString($str, $encoding = self::ENCODING_BASE64)
3197
    {
3198
        $encoded = '';
3199
        switch (strtolower($encoding)) {
3200
            case static::ENCODING_BASE64:
3201
                $encoded = chunk_split(
3202
                    base64_encode($str),
3203
                    static::STD_LINE_LENGTH,
3204
                    static::$LE
3205
                );
3206
                break;
3207
            case static::ENCODING_7BIT:
3208
            case static::ENCODING_8BIT:
3209
                $encoded = static::normalizeBreaks($str);
3210
                // Make sure it ends with a line break
3211
                if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
3212
                    $encoded .= static::$LE;
3213
                }
3214
                break;
3215
            case static::ENCODING_BINARY:
3216
                $encoded = $str;
3217
                break;
3218
            case static::ENCODING_QUOTED_PRINTABLE:
3219
                $encoded = $this->encodeQP($str);
3220
                break;
3221
            default:
3222
                $this->setError($this->lang('encoding') . $encoding);
3223
                if ($this->exceptions) {
3224
                    throw new Exception($this->lang('encoding') . $encoding);
3225
                }
3226
                break;
3227
        }
3228
3229
        return $encoded;
3230
    }
3231
3232
    /**
3233
     * Encode a header value (not including its label) optimally.
3234
     * Picks shortest of Q, B, or none. Result includes folding if needed.
3235
     * See RFC822 definitions for phrase, comment and text positions.
3236
     *
3237
     * @param string $str      The header value to encode
3238
     * @param string $position What context the string will be used in
3239
     *
3240
     * @return string
3241
     */
3242
    public function encodeHeader($str, $position = 'text')
3243
    {
3244
        $matchcount = 0;
3245
        switch (strtolower($position)) {
3246
            case 'phrase':
3247
                if (!preg_match('/[\200-\377]/', $str)) {
3248
                    // Can't use addslashes as we don't know the value of magic_quotes_sybase
3249
                    $encoded = addcslashes($str, "\0..\37\177\\\"");
3250
                    if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
3251
                        return $encoded;
3252
                    }
3253
3254
                    return "\"$encoded\"";
3255
                }
3256
                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
3257
                break;
3258
            /* @noinspection PhpMissingBreakStatementInspection */
3259
            case 'comment':
3260
                $matchcount = preg_match_all('/[()"]/', $str, $matches);
3261
            //fallthrough
3262
            case 'text':
3263
            default:
3264
                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
3265
                break;
3266
        }
3267
3268
        if ($this->has8bitChars($str)) {
3269
            $charset = $this->CharSet;
3270
        } else {
3271
            $charset = static::CHARSET_ASCII;
3272
        }
3273
3274
        // Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
3275
        $overhead = 8 + strlen($charset);
3276
3277
        if ('mail' === $this->Mailer) {
3278
            $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
3279
        } else {
3280
            $maxlen = static::MAX_LINE_LENGTH - $overhead;
3281
        }
3282
3283
        // Select the encoding that produces the shortest output and/or prevents corruption.
3284
        if ($matchcount > strlen($str) / 3) {
3285
            // More than 1/3 of the content needs encoding, use B-encode.
3286
            $encoding = 'B';
3287
        } elseif ($matchcount > 0) {
3288
            // Less than 1/3 of the content needs encoding, use Q-encode.
3289
            $encoding = 'Q';
3290
        } elseif (strlen($str) > $maxlen) {
3291
            // No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
3292
            $encoding = 'Q';
3293
        } else {
3294
            // No reformatting needed
3295
            $encoding = false;
3296
        }
3297
3298
        switch ($encoding) {
3299
            case 'B':
3300
                if ($this->hasMultiBytes($str)) {
3301
                    // Use a custom function which correctly encodes and wraps long
3302
                    // multibyte strings without breaking lines within a character
3303
                    $encoded = $this->base64EncodeWrapMB($str, "\n");
3304
                } else {
3305
                    $encoded = base64_encode($str);
3306
                    $maxlen -= $maxlen % 4;
3307
                    $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
3308
                }
3309
                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
3310
                break;
3311
            case 'Q':
3312
                $encoded = $this->encodeQ($str, $position);
3313
                $encoded = $this->wrapText($encoded, $maxlen, true);
3314
                $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
3315
                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
3316
                break;
3317
            default:
3318
                return $str;
3319
        }
3320
3321
        return trim(static::normalizeBreaks($encoded));
3322
    }
3323
3324
    /**
3325
     * Check if a string contains multi-byte characters.
3326
     *
3327
     * @param string $str multi-byte text to wrap encode
3328
     *
3329
     * @return bool
3330
     */
3331
    public function hasMultiBytes($str)
3332
    {
3333
        if (function_exists('mb_strlen')) {
3334
            return strlen($str) > mb_strlen($str, $this->CharSet);
3335
        }
3336
3337
        // Assume no multibytes (we can't handle without mbstring functions anyway)
3338
        return false;
3339
    }
3340
3341
    /**
3342
     * Does a string contain any 8-bit chars (in any charset)?
3343
     *
3344
     * @param string $text
3345
     *
3346
     * @return bool
3347
     */
3348
    public function has8bitChars($text)
3349
    {
3350
        return (bool) preg_match('/[\x80-\xFF]/', $text);
3351
    }
3352
3353
    /**
3354
     * Encode and wrap long multibyte strings for mail headers
3355
     * without breaking lines within a character.
3356
     * Adapted from a function by paravoid.
3357
     *
3358
     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
3359
     *
3360
     * @param string $str       multi-byte text to wrap encode
3361
     * @param string $linebreak string to use as linefeed/end-of-line
3362
     *
3363
     * @return string
3364
     */
3365
    public function base64EncodeWrapMB($str, $linebreak = null)
3366
    {
3367
        $start = '=?' . $this->CharSet . '?B?';
3368
        $end = '?=';
3369
        $encoded = '';
3370
        if (null === $linebreak) {
3371
            $linebreak = static::$LE;
3372
        }
3373
3374
        $mb_length = mb_strlen($str, $this->CharSet);
3375
        // Each line must have length <= 75, including $start and $end
3376
        $length = 75 - strlen($start) - strlen($end);
3377
        // Average multi-byte ratio
3378
        $ratio = $mb_length / strlen($str);
3379
        // Base64 has a 4:3 ratio
3380
        $avgLength = floor($length * $ratio * .75);
3381
3382
        $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...
3383
        for ($i = 0; $i < $mb_length; $i += $offset) {
3384
            $lookBack = 0;
3385
            do {
3386
                $offset = $avgLength - $lookBack;
3387
                $chunk = mb_substr($str, $i, $offset, $this->CharSet);
3388
                $chunk = base64_encode($chunk);
3389
                ++$lookBack;
3390
            } while (strlen($chunk) > $length);
3391
            $encoded .= $chunk . $linebreak;
3392
        }
3393
3394
        // Chomp the last linefeed
3395
        return substr($encoded, 0, -strlen($linebreak));
3396
    }
3397
3398
    /**
3399
     * Encode a string in quoted-printable format.
3400
     * According to RFC2045 section 6.7.
3401
     *
3402
     * @param string $string The text to encode
3403
     *
3404
     * @return string
3405
     */
3406
    public function encodeQP($string)
3407
    {
3408
        return static::normalizeBreaks(quoted_printable_encode($string));
3409
    }
3410
3411
    /**
3412
     * Encode a string using Q encoding.
3413
     *
3414
     * @see http://tools.ietf.org/html/rfc2047#section-4.2
3415
     *
3416
     * @param string $str      the text to encode
3417
     * @param string $position Where the text is going to be used, see the RFC for what that means
3418
     *
3419
     * @return string
3420
     */
3421
    public function encodeQ($str, $position = 'text')
3422
    {
3423
        // There should not be any EOL in the string
3424
        $pattern = '';
3425
        $encoded = str_replace(["\r", "\n"], '', $str);
3426
        switch (strtolower($position)) {
3427
            case 'phrase':
3428
                // RFC 2047 section 5.3
3429
                $pattern = '^A-Za-z0-9!*+\/ -';
3430
                break;
3431
            /*
3432
             * RFC 2047 section 5.2.
3433
             * Build $pattern without including delimiters and []
3434
             */
3435
            /* @noinspection PhpMissingBreakStatementInspection */
3436
            case 'comment':
3437
                $pattern = '\(\)"';
3438
            /* Intentional fall through */
3439
            case 'text':
3440
            default:
3441
                // RFC 2047 section 5.1
3442
                // Replace every high ascii, control, =, ? and _ characters
3443
                $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
3444
                break;
3445
        }
3446
        $matches = [];
3447
        if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
3448
            // If the string contains an '=', make sure it's the first thing we replace
3449
            // so as to avoid double-encoding
3450
            $eqkey = array_search('=', $matches[0], true);
3451
            if (false !== $eqkey) {
3452
                unset($matches[0][$eqkey]);
3453
                array_unshift($matches[0], '=');
3454
            }
3455
            foreach (array_unique($matches[0]) as $char) {
3456
                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
3457
            }
3458
        }
3459
        // Replace spaces with _ (more readable than =20)
3460
        // RFC 2047 section 4.2(2)
3461
        return str_replace(' ', '_', $encoded);
3462
    }
3463
3464
    /**
3465
     * Add a string or binary attachment (non-filesystem).
3466
     * This method can be used to attach ascii or binary data,
3467
     * such as a BLOB record from a database.
3468
     *
3469
     * @param string $string      String attachment data
3470
     * @param string $filename    Name of the attachment
3471
     * @param string $encoding    File encoding (see $Encoding)
3472
     * @param string $type        File extension (MIME) type
3473
     * @param string $disposition Disposition to use
3474
     *
3475
     * @throws Exception
3476
     *
3477
     * @return bool True on successfully adding an attachment
3478
     */
3479
    public function addStringAttachment(
3480
        $string,
3481
        $filename,
3482
        $encoding = self::ENCODING_BASE64,
3483
        $type = '',
3484
        $disposition = 'attachment'
3485
    ) {
3486
        try {
3487
            // If a MIME type is not specified, try to work it out from the file name
3488
            if ('' === $type) {
3489
                $type = static::filenameToType($filename);
3490
            }
3491
3492
            if (!$this->validateEncoding($encoding)) {
3493
                throw new Exception($this->lang('encoding') . $encoding);
3494
            }
3495
3496
            // Append to $attachment array
3497
            $this->attachment[] = [
3498
                0 => $string,
3499
                1 => $filename,
3500
                2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
3501
                3 => $encoding,
3502
                4 => $type,
3503
                5 => true, // isStringAttachment
3504
                6 => $disposition,
3505
                7 => 0,
3506
            ];
3507
        } catch (Exception $exc) {
3508
            $this->setError($exc->getMessage());
3509
            $this->edebug($exc->getMessage());
3510
            if ($this->exceptions) {
3511
                throw $exc;
3512
            }
3513
3514
            return false;
3515
        }
3516
3517
        return true;
3518
    }
3519
3520
    /**
3521
     * Add an embedded (inline) attachment from a file.
3522
     * This can include images, sounds, and just about any other document type.
3523
     * These differ from 'regular' attachments in that they are intended to be
3524
     * displayed inline with the message, not just attached for download.
3525
     * This is used in HTML messages that embed the images
3526
     * the HTML refers to using the $cid value.
3527
     * Never use a user-supplied path to a file!
3528
     *
3529
     * @param string $path        Path to the attachment
3530
     * @param string $cid         Content ID of the attachment; Use this to reference
3531
     *                            the content when using an embedded image in HTML
3532
     * @param string $name        Overrides the attachment name
3533
     * @param string $encoding    File encoding (see $Encoding)
3534
     * @param string $type        File MIME type
3535
     * @param string $disposition Disposition to use
3536
     *
3537
     * @throws Exception
3538
     *
3539
     * @return bool True on successfully adding an attachment
3540
     */
3541
    public function addEmbeddedImage(
3542
        $path,
3543
        $cid,
3544
        $name = '',
3545
        $encoding = self::ENCODING_BASE64,
3546
        $type = '',
3547
        $disposition = 'inline'
3548
    ) {
3549
        try {
3550
            if (!static::fileIsAccessible($path)) {
3551
                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
3552
            }
3553
3554
            // If a MIME type is not specified, try to work it out from the file name
3555
            if ('' === $type) {
3556
                $type = static::filenameToType($path);
3557
            }
3558
3559
            if (!$this->validateEncoding($encoding)) {
3560
                throw new Exception($this->lang('encoding') . $encoding);
3561
            }
3562
3563
            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
3564
            if ('' === $name) {
3565
                $name = $filename;
3566
            }
3567
3568
            // Append to $attachment array
3569
            $this->attachment[] = [
3570
                0 => $path,
3571
                1 => $filename,
3572
                2 => $name,
3573
                3 => $encoding,
3574
                4 => $type,
3575
                5 => false, // isStringAttachment
3576
                6 => $disposition,
3577
                7 => $cid,
3578
            ];
3579
        } catch (Exception $exc) {
3580
            $this->setError($exc->getMessage());
3581
            $this->edebug($exc->getMessage());
3582
            if ($this->exceptions) {
3583
                throw $exc;
3584
            }
3585
3586
            return false;
3587
        }
3588
3589
        return true;
3590
    }
3591
3592
    /**
3593
     * Add an embedded stringified attachment.
3594
     * This can include images, sounds, and just about any other document type.
3595
     * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.
3596
     *
3597
     * @param string $string      The attachment binary data
3598
     * @param string $cid         Content ID of the attachment; Use this to reference
3599
     *                            the content when using an embedded image in HTML
3600
     * @param string $name        A filename for the attachment. If this contains an extension,
3601
     *                            PHPMailer will attempt to set a MIME type for the attachment.
3602
     *                            For example 'file.jpg' would get an 'image/jpeg' MIME type.
3603
     * @param string $encoding    File encoding (see $Encoding), defaults to 'base64'
3604
     * @param string $type        MIME type - will be used in preference to any automatically derived type
3605
     * @param string $disposition Disposition to use
3606
     *
3607
     * @throws Exception
3608
     *
3609
     * @return bool True on successfully adding an attachment
3610
     */
3611
    public function addStringEmbeddedImage(
3612
        $string,
3613
        $cid,
3614
        $name = '',
3615
        $encoding = self::ENCODING_BASE64,
3616
        $type = '',
3617
        $disposition = 'inline'
3618
    ) {
3619
        try {
3620
            // If a MIME type is not specified, try to work it out from the name
3621
            if ('' === $type && !empty($name)) {
3622
                $type = static::filenameToType($name);
3623
            }
3624
3625
            if (!$this->validateEncoding($encoding)) {
3626
                throw new Exception($this->lang('encoding') . $encoding);
3627
            }
3628
3629
            // Append to $attachment array
3630
            $this->attachment[] = [
3631
                0 => $string,
3632
                1 => $name,
3633
                2 => $name,
3634
                3 => $encoding,
3635
                4 => $type,
3636
                5 => true, // isStringAttachment
3637
                6 => $disposition,
3638
                7 => $cid,
3639
            ];
3640
        } catch (Exception $exc) {
3641
            $this->setError($exc->getMessage());
3642
            $this->edebug($exc->getMessage());
3643
            if ($this->exceptions) {
3644
                throw $exc;
3645
            }
3646
3647
            return false;
3648
        }
3649
3650
        return true;
3651
    }
3652
3653
    /**
3654
     * Validate encodings.
3655
     *
3656
     * @param string $encoding
3657
     *
3658
     * @return bool
3659
     */
3660
    protected function validateEncoding($encoding)
3661
    {
3662
        return in_array(
3663
            $encoding,
3664
            [
3665
                self::ENCODING_7BIT,
3666
                self::ENCODING_QUOTED_PRINTABLE,
3667
                self::ENCODING_BASE64,
3668
                self::ENCODING_8BIT,
3669
                self::ENCODING_BINARY,
3670
            ],
3671
            true
3672
        );
3673
    }
3674
3675
    /**
3676
     * Check if an embedded attachment is present with this cid.
3677
     *
3678
     * @param string $cid
3679
     *
3680
     * @return bool
3681
     */
3682
    protected function cidExists($cid)
3683
    {
3684
        foreach ($this->attachment as $attachment) {
3685
            if ('inline' === $attachment[6] && $cid === $attachment[7]) {
3686
                return true;
3687
            }
3688
        }
3689
3690
        return false;
3691
    }
3692
3693
    /**
3694
     * Check if an inline attachment is present.
3695
     *
3696
     * @return bool
3697
     */
3698
    public function inlineImageExists()
3699
    {
3700
        foreach ($this->attachment as $attachment) {
3701
            if ('inline' === $attachment[6]) {
3702
                return true;
3703
            }
3704
        }
3705
3706
        return false;
3707
    }
3708
3709
    /**
3710
     * Check if an attachment (non-inline) is present.
3711
     *
3712
     * @return bool
3713
     */
3714
    public function attachmentExists()
3715
    {
3716
        foreach ($this->attachment as $attachment) {
3717
            if ('attachment' === $attachment[6]) {
3718
                return true;
3719
            }
3720
        }
3721
3722
        return false;
3723
    }
3724
3725
    /**
3726
     * Check if this message has an alternative body set.
3727
     *
3728
     * @return bool
3729
     */
3730
    public function alternativeExists()
3731
    {
3732
        return !empty($this->AltBody);
3733
    }
3734
3735
    /**
3736
     * Clear queued addresses of given kind.
3737
     *
3738
     * @param string $kind 'to', 'cc', or 'bcc'
3739
     */
3740
    public function clearQueuedAddresses($kind)
3741
    {
3742
        $this->RecipientsQueue = array_filter(
3743
            $this->RecipientsQueue,
3744
            static function ($params) use ($kind) {
3745
                return $params[0] !== $kind;
3746
            }
3747
        );
3748
    }
3749
3750
    /**
3751
     * Clear all To recipients.
3752
     */
3753
    public function clearAddresses()
3754
    {
3755
        foreach ($this->to as $to) {
3756
            unset($this->all_recipients[strtolower($to[0])]);
3757
        }
3758
        $this->to = [];
3759
        $this->clearQueuedAddresses('to');
3760
    }
3761
3762
    /**
3763
     * Clear all CC recipients.
3764
     */
3765
    public function clearCCs()
3766
    {
3767
        foreach ($this->cc as $cc) {
3768
            unset($this->all_recipients[strtolower($cc[0])]);
3769
        }
3770
        $this->cc = [];
3771
        $this->clearQueuedAddresses('cc');
3772
    }
3773
3774
    /**
3775
     * Clear all BCC recipients.
3776
     */
3777
    public function clearBCCs()
3778
    {
3779
        foreach ($this->bcc as $bcc) {
3780
            unset($this->all_recipients[strtolower($bcc[0])]);
3781
        }
3782
        $this->bcc = [];
3783
        $this->clearQueuedAddresses('bcc');
3784
    }
3785
3786
    /**
3787
     * Clear all ReplyTo recipients.
3788
     */
3789
    public function clearReplyTos()
3790
    {
3791
        $this->ReplyTo = [];
3792
        $this->ReplyToQueue = [];
3793
    }
3794
3795
    /**
3796
     * Clear all recipient types.
3797
     */
3798
    public function clearAllRecipients()
3799
    {
3800
        $this->to = [];
3801
        $this->cc = [];
3802
        $this->bcc = [];
3803
        $this->all_recipients = [];
3804
        $this->RecipientsQueue = [];
3805
    }
3806
3807
    /**
3808
     * Clear all filesystem, string, and binary attachments.
3809
     */
3810
    public function clearAttachments()
3811
    {
3812
        $this->attachment = [];
3813
    }
3814
3815
    /**
3816
     * Clear all custom headers.
3817
     */
3818
    public function clearCustomHeaders()
3819
    {
3820
        $this->CustomHeader = [];
3821
    }
3822
3823
    /**
3824
     * Add an error message to the error container.
3825
     *
3826
     * @param string $msg
3827
     */
3828
    protected function setError($msg)
3829
    {
3830
        ++$this->error_count;
3831
        if ('smtp' === $this->Mailer && null !== $this->smtp) {
3832
            $lasterror = $this->smtp->getError();
3833
            if (!empty($lasterror['error'])) {
3834
                $msg .= $this->lang('smtp_error') . $lasterror['error'];
3835
                if (!empty($lasterror['detail'])) {
3836
                    $msg .= ' Detail: ' . $lasterror['detail'];
3837
                }
3838
                if (!empty($lasterror['smtp_code'])) {
3839
                    $msg .= ' SMTP code: ' . $lasterror['smtp_code'];
3840
                }
3841
                if (!empty($lasterror['smtp_code_ex'])) {
3842
                    $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex'];
3843
                }
3844
            }
3845
        }
3846
        $this->ErrorInfo = $msg;
3847
    }
3848
3849
    /**
3850
     * Return an RFC 822 formatted date.
3851
     *
3852
     * @return string
3853
     */
3854
    public static function rfcDate()
3855
    {
3856
        // Set the time zone to whatever the default is to avoid 500 errors
3857
        // Will default to UTC if it's not set properly in php.ini
3858
        date_default_timezone_set(@date_default_timezone_get());
3859
3860
        return date('D, j M Y H:i:s O');
3861
    }
3862
3863
    /**
3864
     * Get the server hostname.
3865
     * Returns 'localhost.localdomain' if unknown.
3866
     *
3867
     * @return string
3868
     */
3869
    protected function serverHostname()
3870
    {
3871
        $result = '';
3872
        if (!empty($this->Hostname)) {
3873
            $result = $this->Hostname;
3874
        } elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) {
3875
            $result = $_SERVER['SERVER_NAME'];
3876
        } elseif (function_exists('gethostname') && gethostname() !== false) {
3877
            $result = gethostname();
3878
        } elseif (php_uname('n') !== false) {
3879
            $result = php_uname('n');
3880
        }
3881
        if (!static::isValidHost($result)) {
3882
            return 'localhost.localdomain';
3883
        }
3884
3885
        return $result;
3886
    }
3887
3888
    /**
3889
     * Validate whether a string contains a valid value to use as a hostname or IP address.
3890
     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
3891
     *
3892
     * @param string $host The host name or IP address to check
3893
     *
3894
     * @return bool
3895
     */
3896
    public static function isValidHost($host)
3897
    {
3898
        //Simple syntax limits
3899
        if (empty($host)
3900
            || !is_string($host)
3901
            || strlen($host) > 256
3902
            || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+])$/', $host)
3903
        ) {
3904
            return false;
3905
        }
3906
        //Looks like a bracketed IPv6 address
3907
        if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') {
3908
            return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
3909
        }
3910
        //If removing all the dots results in a numeric string, it must be an IPv4 address.
3911
        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
3912
        if (is_numeric(str_replace('.', '', $host))) {
3913
            //Is it a valid IPv4 address?
3914
            return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
3915
        }
3916
        if (filter_var('http://' . $host, FILTER_VALIDATE_URL) !== false) {
3917
            //Is it a syntactically valid hostname?
3918
            return true;
3919
        }
3920
3921
        return false;
3922
    }
3923
3924
    /**
3925
     * Get an error message in the current language.
3926
     *
3927
     * @param string $key
3928
     *
3929
     * @return string
3930
     */
3931
    protected function lang($key)
3932
    {
3933
        if (count($this->language) < 1) {
3934
            $this->setLanguage(); // set the default language
3935
        }
3936
3937
        if (array_key_exists($key, $this->language)) {
3938
            if ('smtp_connect_failed' === $key) {
3939
                //Include a link to troubleshooting docs on SMTP connection failure
3940
                //this is by far the biggest cause of support questions
3941
                //but it's usually not PHPMailer's fault.
3942
                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
3943
            }
3944
3945
            return $this->language[$key];
3946
        }
3947
3948
        //Return the key as a fallback
3949
        return $key;
3950
    }
3951
3952
    /**
3953
     * Check if an error occurred.
3954
     *
3955
     * @return bool True if an error did occur
3956
     */
3957
    public function isError()
3958
    {
3959
        return $this->error_count > 0;
3960
    }
3961
3962
    /**
3963
     * Add a custom header.
3964
     * $name value can be overloaded to contain
3965
     * both header name and value (name:value).
3966
     *
3967
     * @param string      $name  Custom header name
3968
     * @param string|null $value Header value
3969
     *
3970
     * @throws Exception
3971
     */
3972
    public function addCustomHeader($name, $value = null)
3973
    {
3974
        if (null === $value && strpos($name, ':') !== false) {
3975
            // Value passed in as name:value
3976
            list($name, $value) = explode(':', $name, 2);
3977
        }
3978
        $name = trim($name);
3979
        $value = trim($value);
3980
        //Ensure name is not empty, and that neither name nor value contain line breaks
3981
        if (empty($name) || strpbrk($name . $value, "\r\n") !== false) {
3982
            if ($this->exceptions) {
3983
                throw new Exception('Invalid header name or value');
3984
            }
3985
3986
            return false;
3987
        }
3988
        $this->CustomHeader[] = [$name, $value];
3989
3990
        return true;
3991
    }
3992
3993
    /**
3994
     * Returns all custom headers.
3995
     *
3996
     * @return array
3997
     */
3998
    public function getCustomHeaders()
3999
    {
4000
        return $this->CustomHeader;
4001
    }
4002
4003
    /**
4004
     * Create a message body from an HTML string.
4005
     * Automatically inlines images and creates a plain-text version by converting the HTML,
4006
     * overwriting any existing values in Body and AltBody.
4007
     * Do not source $message content from user input!
4008
     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
4009
     * will look for an image file in $basedir/images/a.png and convert it to inline.
4010
     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
4011
     * Converts data-uri images into embedded attachments.
4012
     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
4013
     *
4014
     * @param string        $message  HTML message string
4015
     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
4016
     * @param bool|callable $advanced Whether to use the internal HTML to text converter
4017
     *                                or your own custom converter
4018
     * @return string The transformed message body
4019
     *
4020
     * @throws Exception
4021
     *
4022
     * @see PHPMailer::html2text()
4023
     */
4024
    public function msgHTML($message, $basedir = '', $advanced = false)
4025
    {
4026
        preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
4027
        if (array_key_exists(2, $images)) {
4028
            if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
4029
                // Ensure $basedir has a trailing /
4030
                $basedir .= '/';
4031
            }
4032
            foreach ($images[2] as $imgindex => $url) {
4033
                // Convert data URIs into embedded images
4034
                //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
4035
                $match = [];
4036
                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
4037
                    if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
4038
                        $data = base64_decode($match[3]);
4039
                    } elseif ('' === $match[2]) {
4040
                        $data = rawurldecode($match[3]);
4041
                    } else {
4042
                        //Not recognised so leave it alone
4043
                        continue;
4044
                    }
4045
                    //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
4046
                    //will only be embedded once, even if it used a different encoding
4047
                    $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; // RFC2392 S 2
4048
4049
                    if (!$this->cidExists($cid)) {
4050
                        $this->addStringEmbeddedImage(
4051
                            $data,
4052
                            $cid,
4053
                            'embed' . $imgindex,
4054
                            static::ENCODING_BASE64,
4055
                            $match[1]
4056
                        );
4057
                    }
4058
                    $message = str_replace(
4059
                        $images[0][$imgindex],
4060
                        $images[1][$imgindex] . '="cid:' . $cid . '"',
4061
                        $message
4062
                    );
4063
                    continue;
4064
                }
4065
                if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
4066
                    !empty($basedir)
4067
                    // Ignore URLs containing parent dir traversal (..)
4068
                    && (strpos($url, '..') === false)
4069
                    // Do not change urls that are already inline images
4070
                    && 0 !== strpos($url, 'cid:')
4071
                    // Do not change absolute URLs, including anonymous protocol
4072
                    && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
4073
                ) {
4074
                    $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
4075
                    $directory = dirname($url);
4076
                    if ('.' === $directory) {
4077
                        $directory = '';
4078
                    }
4079
                    // RFC2392 S 2
4080
                    $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
4081
                    if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
4082
                        $basedir .= '/';
4083
                    }
4084
                    if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
4085
                        $directory .= '/';
4086
                    }
4087
                    if ($this->addEmbeddedImage(
4088
                        $basedir . $directory . $filename,
4089
                        $cid,
4090
                        $filename,
0 ignored issues
show
Bug introduced by
It seems like $filename defined by static::mb_pathinfo($url, PATHINFO_BASENAME) on line 4074 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...
4091
                        static::ENCODING_BASE64,
4092
                        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 4074 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...
4093
                    )
4094
                    ) {
4095
                        $message = preg_replace(
4096
                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
4097
                            $images[1][$imgindex] . '="cid:' . $cid . '"',
4098
                            $message
4099
                        );
4100
                    }
4101
                }
4102
            }
4103
        }
4104
        $this->isHTML();
4105
        // Convert all message body line breaks to LE, makes quoted-printable encoding work much better
4106
        $this->Body = static::normalizeBreaks($message);
4107
        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
4108
        if (!$this->alternativeExists()) {
4109
            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
4110
                . static::$LE;
4111
        }
4112
4113
        return $this->Body;
4114
    }
4115
4116
    /**
4117
     * Convert an HTML string into plain text.
4118
     * This is used by msgHTML().
4119
     * Note - older versions of this function used a bundled advanced converter
4120
     * which was removed for license reasons in #232.
4121
     * Example usage:
4122
     *
4123
     * ```php
4124
     * // Use default conversion
4125
     * $plain = $mail->html2text($html);
4126
     * // Use your own custom converter
4127
     * $plain = $mail->html2text($html, function($html) {
4128
     *     $converter = new MyHtml2text($html);
4129
     *     return $converter->get_text();
4130
     * });
4131
     * ```
4132
     *
4133
     * @param string        $html     The HTML text to convert
4134
     * @param bool|callable $advanced Any boolean value to use the internal converter,
4135
     *                                or provide your own callable for custom conversion
4136
     *
4137
     * @return string
4138
     */
4139
    public function html2text($html, $advanced = false)
4140
    {
4141
        if (is_callable($advanced)) {
4142
            return call_user_func($advanced, $html);
4143
        }
4144
4145
        return html_entity_decode(
4146
            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
4147
            ENT_QUOTES,
4148
            $this->CharSet
4149
        );
4150
    }
4151
4152
    /**
4153
     * Get the MIME type for a file extension.
4154
     *
4155
     * @param string $ext File extension
4156
     *
4157
     * @return string MIME type of file
4158
     */
4159
    public static function _mime_types($ext = '')
4160
    {
4161
        $mimes = [
4162
            'xl' => 'application/excel',
4163
            'js' => 'application/javascript',
4164
            'hqx' => 'application/mac-binhex40',
4165
            'cpt' => 'application/mac-compactpro',
4166
            'bin' => 'application/macbinary',
4167
            'doc' => 'application/msword',
4168
            'word' => 'application/msword',
4169
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
4170
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
4171
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
4172
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
4173
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
4174
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
4175
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
4176
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
4177
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
4178
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
4179
            'class' => 'application/octet-stream',
4180
            'dll' => 'application/octet-stream',
4181
            'dms' => 'application/octet-stream',
4182
            'exe' => 'application/octet-stream',
4183
            'lha' => 'application/octet-stream',
4184
            'lzh' => 'application/octet-stream',
4185
            'psd' => 'application/octet-stream',
4186
            'sea' => 'application/octet-stream',
4187
            'so' => 'application/octet-stream',
4188
            'oda' => 'application/oda',
4189
            'pdf' => 'application/pdf',
4190
            'ai' => 'application/postscript',
4191
            'eps' => 'application/postscript',
4192
            'ps' => 'application/postscript',
4193
            'smi' => 'application/smil',
4194
            'smil' => 'application/smil',
4195
            'mif' => 'application/vnd.mif',
4196
            'xls' => 'application/vnd.ms-excel',
4197
            'ppt' => 'application/vnd.ms-powerpoint',
4198
            'wbxml' => 'application/vnd.wap.wbxml',
4199
            'wmlc' => 'application/vnd.wap.wmlc',
4200
            'dcr' => 'application/x-director',
4201
            'dir' => 'application/x-director',
4202
            'dxr' => 'application/x-director',
4203
            'dvi' => 'application/x-dvi',
4204
            'gtar' => 'application/x-gtar',
4205
            'php3' => 'application/x-httpd-php',
4206
            'php4' => 'application/x-httpd-php',
4207
            'php' => 'application/x-httpd-php',
4208
            'phtml' => 'application/x-httpd-php',
4209
            'phps' => 'application/x-httpd-php-source',
4210
            'swf' => 'application/x-shockwave-flash',
4211
            'sit' => 'application/x-stuffit',
4212
            'tar' => 'application/x-tar',
4213
            'tgz' => 'application/x-tar',
4214
            'xht' => 'application/xhtml+xml',
4215
            'xhtml' => 'application/xhtml+xml',
4216
            'zip' => 'application/zip',
4217
            'mid' => 'audio/midi',
4218
            'midi' => 'audio/midi',
4219
            'mp2' => 'audio/mpeg',
4220
            'mp3' => 'audio/mpeg',
4221
            'm4a' => 'audio/mp4',
4222
            'mpga' => 'audio/mpeg',
4223
            'aif' => 'audio/x-aiff',
4224
            'aifc' => 'audio/x-aiff',
4225
            'aiff' => 'audio/x-aiff',
4226
            'ram' => 'audio/x-pn-realaudio',
4227
            'rm' => 'audio/x-pn-realaudio',
4228
            'rpm' => 'audio/x-pn-realaudio-plugin',
4229
            'ra' => 'audio/x-realaudio',
4230
            'wav' => 'audio/x-wav',
4231
            'mka' => 'audio/x-matroska',
4232
            'bmp' => 'image/bmp',
4233
            'gif' => 'image/gif',
4234
            'jpeg' => 'image/jpeg',
4235
            'jpe' => 'image/jpeg',
4236
            'jpg' => 'image/jpeg',
4237
            'png' => 'image/png',
4238
            'tiff' => 'image/tiff',
4239
            'tif' => 'image/tiff',
4240
            'webp' => 'image/webp',
4241
            'avif' => 'image/avif',
4242
            'heif' => 'image/heif',
4243
            'heifs' => 'image/heif-sequence',
4244
            'heic' => 'image/heic',
4245
            'heics' => 'image/heic-sequence',
4246
            'eml' => 'message/rfc822',
4247
            'css' => 'text/css',
4248
            'html' => 'text/html',
4249
            'htm' => 'text/html',
4250
            'shtml' => 'text/html',
4251
            'log' => 'text/plain',
4252
            'text' => 'text/plain',
4253
            'txt' => 'text/plain',
4254
            'rtx' => 'text/richtext',
4255
            'rtf' => 'text/rtf',
4256
            'vcf' => 'text/vcard',
4257
            'vcard' => 'text/vcard',
4258
            'ics' => 'text/calendar',
4259
            'xml' => 'text/xml',
4260
            'xsl' => 'text/xml',
4261
            'wmv' => 'video/x-ms-wmv',
4262
            'mpeg' => 'video/mpeg',
4263
            'mpe' => 'video/mpeg',
4264
            'mpg' => 'video/mpeg',
4265
            'mp4' => 'video/mp4',
4266
            'm4v' => 'video/mp4',
4267
            'mov' => 'video/quicktime',
4268
            'qt' => 'video/quicktime',
4269
            'rv' => 'video/vnd.rn-realvideo',
4270
            'avi' => 'video/x-msvideo',
4271
            'movie' => 'video/x-sgi-movie',
4272
            'webm' => 'video/webm',
4273
            'mkv' => 'video/x-matroska',
4274
        ];
4275
        $ext = strtolower($ext);
4276
        if (array_key_exists($ext, $mimes)) {
4277
            return $mimes[$ext];
4278
        }
4279
4280
        return 'application/octet-stream';
4281
    }
4282
4283
    /**
4284
     * Map a file name to a MIME type.
4285
     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
4286
     *
4287
     * @param string $filename A file name or full path, does not need to exist as a file
4288
     *
4289
     * @return string
4290
     */
4291
    public static function filenameToType($filename)
4292
    {
4293
        // In case the path is a URL, strip any query string before getting extension
4294
        $qpos = strpos($filename, '?');
4295
        if (false !== $qpos) {
4296
            $filename = substr($filename, 0, $qpos);
4297
        }
4298
        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
4299
4300
        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 4298 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...
4301
    }
4302
4303
    /**
4304
     * Multi-byte-safe pathinfo replacement.
4305
     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
4306
     *
4307
     * @see http://www.php.net/manual/en/function.pathinfo.php#107461
4308
     *
4309
     * @param string     $path    A filename or path, does not need to exist as a file
4310
     * @param int|string $options Either a PATHINFO_* constant,
4311
     *                            or a string name to return only the specified piece
4312
     *
4313
     * @return string|array
4314
     */
4315
    public static function mb_pathinfo($path, $options = null)
4316
    {
4317
        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
4318
        $pathinfo = [];
4319
        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
4320
            if (array_key_exists(1, $pathinfo)) {
4321
                $ret['dirname'] = $pathinfo[1];
4322
            }
4323
            if (array_key_exists(2, $pathinfo)) {
4324
                $ret['basename'] = $pathinfo[2];
4325
            }
4326
            if (array_key_exists(5, $pathinfo)) {
4327
                $ret['extension'] = $pathinfo[5];
4328
            }
4329
            if (array_key_exists(3, $pathinfo)) {
4330
                $ret['filename'] = $pathinfo[3];
4331
            }
4332
        }
4333
        switch ($options) {
4334
            case PATHINFO_DIRNAME:
4335
            case 'dirname':
4336
                return $ret['dirname'];
4337
            case PATHINFO_BASENAME:
4338
            case 'basename':
4339
                return $ret['basename'];
4340
            case PATHINFO_EXTENSION:
4341
            case 'extension':
4342
                return $ret['extension'];
4343
            case PATHINFO_FILENAME:
4344
            case 'filename':
4345
                return $ret['filename'];
4346
            default:
4347
                return $ret;
4348
        }
4349
    }
4350
4351
    /**
4352
     * Set or reset instance properties.
4353
     * You should avoid this function - it's more verbose, less efficient, more error-prone and
4354
     * harder to debug than setting properties directly.
4355
     * Usage Example:
4356
     * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`
4357
     *   is the same as:
4358
     * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.
4359
     *
4360
     * @param string $name  The property name to set
4361
     * @param mixed  $value The value to set the property to
4362
     *
4363
     * @return bool
4364
     */
4365
    public function set($name, $value = '')
4366
    {
4367
        if (property_exists($this, $name)) {
4368
            $this->$name = $value;
4369
4370
            return true;
4371
        }
4372
        $this->setError($this->lang('variable_set') . $name);
4373
4374
        return false;
4375
    }
4376
4377
    /**
4378
     * Strip newlines to prevent header injection.
4379
     *
4380
     * @param string $str
4381
     *
4382
     * @return string
4383
     */
4384
    public function secureHeader($str)
4385
    {
4386
        return trim(str_replace(["\r", "\n"], '', $str));
4387
    }
4388
4389
    /**
4390
     * Normalize line breaks in a string.
4391
     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
4392
     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
4393
     *
4394
     * @param string $text
4395
     * @param string $breaktype What kind of line break to use; defaults to static::$LE
4396
     *
4397
     * @return string
4398
     */
4399
    public static function normalizeBreaks($text, $breaktype = null)
4400
    {
4401
        if (null === $breaktype) {
4402
            $breaktype = static::$LE;
4403
        }
4404
        // Normalise to \n
4405
        $text = str_replace([self::CRLF, "\r"], "\n", $text);
4406
        // Now convert LE as needed
4407
        if ("\n" !== $breaktype) {
4408
            $text = str_replace("\n", $breaktype, $text);
4409
        }
4410
4411
        return $text;
4412
    }
4413
4414
    /**
4415
     * Remove trailing breaks from a string.
4416
     *
4417
     * @param string $text
4418
     *
4419
     * @return string The text to remove breaks from
4420
     */
4421
    public static function stripTrailingWSP($text)
4422
    {
4423
        return rtrim($text, " \r\n\t");
4424
    }
4425
4426
    /**
4427
     * Return the current line break format string.
4428
     *
4429
     * @return string
4430
     */
4431
    public static function getLE()
4432
    {
4433
        return static::$LE;
4434
    }
4435
4436
    /**
4437
     * Set the line break format string, e.g. "\r\n".
4438
     *
4439
     * @param string $le
4440
     */
4441
    protected static function setLE($le)
4442
    {
4443
        static::$LE = $le;
4444
    }
4445
4446
    /**
4447
     * Set the public and private key files and password for S/MIME signing.
4448
     *
4449
     * @param string $cert_filename
4450
     * @param string $key_filename
4451
     * @param string $key_pass            Password for private key
4452
     * @param string $extracerts_filename Optional path to chain certificate
4453
     */
4454
    public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
4455
    {
4456
        $this->sign_cert_file = $cert_filename;
4457
        $this->sign_key_file = $key_filename;
4458
        $this->sign_key_pass = $key_pass;
4459
        $this->sign_extracerts_file = $extracerts_filename;
4460
    }
4461
4462
    /**
4463
     * Quoted-Printable-encode a DKIM header.
4464
     *
4465
     * @param string $txt
4466
     *
4467
     * @return string
4468
     */
4469
    public function DKIM_QP($txt)
4470
    {
4471
        $line = '';
4472
        $len = strlen($txt);
4473
        for ($i = 0; $i < $len; ++$i) {
4474
            $ord = ord($txt[$i]);
4475
            if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
4476
                $line .= $txt[$i];
4477
            } else {
4478
                $line .= '=' . sprintf('%02X', $ord);
4479
            }
4480
        }
4481
4482
        return $line;
4483
    }
4484
4485
    /**
4486
     * Generate a DKIM signature.
4487
     *
4488
     * @param string $signHeader
4489
     *
4490
     * @throws Exception
4491
     *
4492
     * @return string The DKIM signature value
4493
     */
4494
    public function DKIM_Sign($signHeader)
4495
    {
4496
        if (!defined('PKCS7_TEXT')) {
4497
            if ($this->exceptions) {
4498
                throw new Exception($this->lang('extension_missing') . 'openssl');
4499
            }
4500
4501
            return '';
4502
        }
4503
        $privKeyStr = !empty($this->DKIM_private_string) ?
4504
            $this->DKIM_private_string :
4505
            file_get_contents($this->DKIM_private);
4506
        if ('' !== $this->DKIM_passphrase) {
4507
            $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
4508
        } else {
4509
            $privKey = openssl_pkey_get_private($privKeyStr);
4510
        }
4511
        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
4512
            openssl_pkey_free($privKey);
4513
4514
            return base64_encode($signature);
4515
        }
4516
        openssl_pkey_free($privKey);
4517
4518
        return '';
4519
    }
4520
4521
    /**
4522
     * Generate a DKIM canonicalization header.
4523
     * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
4524
     * Canonicalized headers should *always* use CRLF, regardless of mailer setting.
4525
     *
4526
     * @see https://tools.ietf.org/html/rfc6376#section-3.4.2
4527
     *
4528
     * @param string $signHeader Header
4529
     *
4530
     * @return string
4531
     */
4532
    public function DKIM_HeaderC($signHeader)
4533
    {
4534
        //Normalize breaks to CRLF (regardless of the mailer)
4535
        $signHeader = static::normalizeBreaks($signHeader, self::CRLF);
4536
        //Unfold header lines
4537
        //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
4538
        //@see https://tools.ietf.org/html/rfc5322#section-2.2
4539
        //That means this may break if you do something daft like put vertical tabs in your headers.
4540
        $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
4541
        //Break headers out into an array
4542
        $lines = explode(self::CRLF, $signHeader);
4543
        foreach ($lines as $key => $line) {
4544
            //If the header is missing a :, skip it as it's invalid
4545
            //This is likely to happen because the explode() above will also split
4546
            //on the trailing LE, leaving an empty line
4547
            if (strpos($line, ':') === false) {
4548
                continue;
4549
            }
4550
            list($heading, $value) = explode(':', $line, 2);
4551
            //Lower-case header name
4552
            $heading = strtolower($heading);
4553
            //Collapse white space within the value, also convert WSP to space
4554
            $value = preg_replace('/[ \t]+/', ' ', $value);
4555
            //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
4556
            //But then says to delete space before and after the colon.
4557
            //Net result is the same as trimming both ends of the value.
4558
            //By elimination, the same applies to the field name
4559
            $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
4560
        }
4561
4562
        return implode(self::CRLF, $lines);
4563
    }
4564
4565
    /**
4566
     * Generate a DKIM canonicalization body.
4567
     * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
4568
     * Canonicalized bodies should *always* use CRLF, regardless of mailer setting.
4569
     *
4570
     * @see https://tools.ietf.org/html/rfc6376#section-3.4.3
4571
     *
4572
     * @param string $body Message Body
4573
     *
4574
     * @return string
4575
     */
4576
    public function DKIM_BodyC($body)
4577
    {
4578
        if (empty($body)) {
4579
            return self::CRLF;
4580
        }
4581
        // Normalize line endings to CRLF
4582
        $body = static::normalizeBreaks($body, self::CRLF);
4583
4584
        //Reduce multiple trailing line breaks to a single one
4585
        return static::stripTrailingWSP($body) . self::CRLF;
4586
    }
4587
4588
    /**
4589
     * Create the DKIM header and body in a new message header.
4590
     *
4591
     * @param string $headers_line Header lines
4592
     * @param string $subject      Subject
4593
     * @param string $body         Body
4594
     *
4595
     * @throws Exception
4596
     *
4597
     * @return string
4598
     */
4599
    public function DKIM_Add($headers_line, $subject, $body)
4600
    {
4601
        $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms
4602
        $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization methods of header & body
4603
        $DKIMquery = 'dns/txt'; // Query method
4604
        $DKIMtime = time();
4605
        //Always sign these headers without being asked
4606
        //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1
4607
        $autoSignHeaders = [
4608
            'from',
4609
            'to',
4610
            'cc',
4611
            'date',
4612
            'subject',
4613
            'reply-to',
4614
            'message-id',
4615
            'content-type',
4616
            'mime-version',
4617
            'x-mailer',
4618
        ];
4619
        if (stripos($headers_line, 'Subject') === false) {
4620
            $headers_line .= 'Subject: ' . $subject . static::$LE;
4621
        }
4622
        $headerLines = explode(static::$LE, $headers_line);
4623
        $currentHeaderLabel = '';
4624
        $currentHeaderValue = '';
4625
        $parsedHeaders = [];
4626
        $headerLineIndex = 0;
4627
        $headerLineCount = count($headerLines);
4628
        foreach ($headerLines as $headerLine) {
4629
            $matches = [];
4630
            if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
4631
                if ($currentHeaderLabel !== '') {
4632
                    //We were previously in another header; This is the start of a new header, so save the previous one
4633
                    $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
4634
                }
4635
                $currentHeaderLabel = $matches[1];
4636
                $currentHeaderValue = $matches[2];
4637
            } elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
4638
                //This is a folded continuation of the current header, so unfold it
4639
                $currentHeaderValue .= ' ' . $matches[1];
4640
            }
4641
            ++$headerLineIndex;
4642
            if ($headerLineIndex >= $headerLineCount) {
4643
                //This was the last line, so finish off this header
4644
                $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
4645
            }
4646
        }
4647
        $copiedHeaders = [];
4648
        $headersToSignKeys = [];
4649
        $headersToSign = [];
4650
        foreach ($parsedHeaders as $header) {
4651
            //Is this header one that must be included in the DKIM signature?
4652
            if (in_array(strtolower($header['label']), $autoSignHeaders, true)) {
4653
                $headersToSignKeys[] = $header['label'];
4654
                $headersToSign[] = $header['label'] . ': ' . $header['value'];
4655
                if ($this->DKIM_copyHeaderFields) {
4656
                    $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
4657
                        str_replace('|', '=7C', $this->DKIM_QP($header['value']));
4658
                }
4659
                continue;
4660
            }
4661
            //Is this an extra custom header we've been asked to sign?
4662
            if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {
4663
                //Find its value in custom headers
4664
                foreach ($this->CustomHeader as $customHeader) {
4665
                    if ($customHeader[0] === $header['label']) {
4666
                        $headersToSignKeys[] = $header['label'];
4667
                        $headersToSign[] = $header['label'] . ': ' . $header['value'];
4668
                        if ($this->DKIM_copyHeaderFields) {
4669
                            $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
4670
                                str_replace('|', '=7C', $this->DKIM_QP($header['value']));
4671
                        }
4672
                        //Skip straight to the next header
4673
                        continue 2;
4674
                    }
4675
                }
4676
            }
4677
        }
4678
        $copiedHeaderFields = '';
4679
        if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {
4680
            //Assemble a DKIM 'z' tag
4681
            $copiedHeaderFields = ' z=';
4682
            $first = true;
4683
            foreach ($copiedHeaders as $copiedHeader) {
4684
                if (!$first) {
4685
                    $copiedHeaderFields .= static::$LE . ' |';
4686
                }
4687
                //Fold long values
4688
                if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {
4689
                    $copiedHeaderFields .= substr(
4690
                        chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS),
4691
                        0,
4692
                        -strlen(static::$LE . self::FWS)
4693
                    );
4694
                } else {
4695
                    $copiedHeaderFields .= $copiedHeader;
4696
                }
4697
                $first = false;
4698
            }
4699
            $copiedHeaderFields .= ';' . static::$LE;
4700
        }
4701
        $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
4702
        $headerValues = implode(static::$LE, $headersToSign);
4703
        $body = $this->DKIM_BodyC($body);
4704
        $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body
4705
        $ident = '';
4706
        if ('' !== $this->DKIM_identity) {
4707
            $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
4708
        }
4709
        //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
4710
        //which is appended after calculating the signature
4711
        //https://tools.ietf.org/html/rfc6376#section-3.5
4712
        $dkimSignatureHeader = 'DKIM-Signature: v=1;' .
4713
            ' d=' . $this->DKIM_domain . ';' .
4714
            ' s=' . $this->DKIM_selector . ';' . static::$LE .
4715
            ' a=' . $DKIMsignatureType . ';' .
4716
            ' q=' . $DKIMquery . ';' .
4717
            ' t=' . $DKIMtime . ';' .
4718
            ' c=' . $DKIMcanonicalization . ';' . static::$LE .
4719
            $headerKeys .
4720
            $ident .
4721
            $copiedHeaderFields .
4722
            ' bh=' . $DKIMb64 . ';' . static::$LE .
4723
            ' b=';
4724
        //Canonicalize the set of headers
4725
        $canonicalizedHeaders = $this->DKIM_HeaderC(
4726
            $headerValues . static::$LE . $dkimSignatureHeader
4727
        );
4728
        $signature = $this->DKIM_Sign($canonicalizedHeaders);
4729
        $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS));
4730
4731
        return static::normalizeBreaks($dkimSignatureHeader . $signature);
4732
    }
4733
4734
    /**
4735
     * Detect if a string contains a line longer than the maximum line length
4736
     * allowed by RFC 2822 section 2.1.1.
4737
     *
4738
     * @param string $str
4739
     *
4740
     * @return bool
4741
     */
4742
    public static function hasLineLongerThanMax($str)
4743
    {
4744
        return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
4745
    }
4746
4747
    /**
4748
     * If a string contains any "special" characters, double-quote the name,
4749
     * and escape any double quotes with a backslash.
4750
     *
4751
     * @param string $str
4752
     *
4753
     * @return string
4754
     *
4755
     * @see RFC822 3.4.1
4756
     */
4757
    public static function quotedString($str)
4758
    {
4759
        if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) {
4760
            //If the string contains any of these chars, it must be double-quoted
4761
            //and any double quotes must be escaped with a backslash
4762
            return '"' . str_replace('"', '\\"', $str) . '"';
4763
        }
4764
4765
        //Return the string untouched, it doesn't need quoting
4766
        return $str;
4767
    }
4768
4769
    /**
4770
     * Allows for public read access to 'to' property.
4771
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4772
     *
4773
     * @return array
4774
     */
4775
    public function getToAddresses()
4776
    {
4777
        return $this->to;
4778
    }
4779
4780
    /**
4781
     * Allows for public read access to 'cc' property.
4782
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4783
     *
4784
     * @return array
4785
     */
4786
    public function getCcAddresses()
4787
    {
4788
        return $this->cc;
4789
    }
4790
4791
    /**
4792
     * Allows for public read access to 'bcc' property.
4793
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4794
     *
4795
     * @return array
4796
     */
4797
    public function getBccAddresses()
4798
    {
4799
        return $this->bcc;
4800
    }
4801
4802
    /**
4803
     * Allows for public read access to 'ReplyTo' property.
4804
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4805
     *
4806
     * @return array
4807
     */
4808
    public function getReplyToAddresses()
4809
    {
4810
        return $this->ReplyTo;
4811
    }
4812
4813
    /**
4814
     * Allows for public read access to 'all_recipients' property.
4815
     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
4816
     *
4817
     * @return array
4818
     */
4819
    public function getAllRecipientAddresses()
4820
    {
4821
        return $this->all_recipients;
4822
    }
4823
4824
    /**
4825
     * Perform a callback.
4826
     *
4827
     * @param bool   $isSent
4828
     * @param array  $to
4829
     * @param array  $cc
4830
     * @param array  $bcc
4831
     * @param string $subject
4832
     * @param string $body
4833
     * @param string $from
4834
     * @param array  $extra
4835
     */
4836
    protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
4837
    {
4838
        if (!empty($this->action_function) && is_callable($this->action_function)) {
4839
            call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);
4840
        }
4841
    }
4842
4843
    /**
4844
     * Get the OAuth instance.
4845
     *
4846
     * @return OAuth
4847
     */
4848
    public function getOAuth()
4849
    {
4850
        return $this->oauth;
4851
    }
4852
4853
    /**
4854
     * Set an OAuth instance.
4855
     */
4856
    public function setOAuth(OAuth $oauth)
4857
    {
4858
        $this->oauth = $oauth;
4859
    }
4860
}
4861