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