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