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