Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ignitetch
GitHub Repository: ignitetch/advphishing
Path: blob/master/PHPMailer/src/PHPMailer.php
738 views
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) {
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) {
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) {
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) {
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;
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,
4091
static::ENCODING_BASE64,
4092
static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
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);
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
4862