[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/PHPMailer/ -> PHPMailer.php (source)

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


Generated : Wed May 6 08:20:15 2026 Cross-referenced by PHPXref