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