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


Generated : Sat Nov 23 08:20:01 2024 Cross-referenced by PHPXref