[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Fri Oct 10 08:20:03 2025 Cross-referenced by PHPXref