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


Generated : Fri Apr 26 08:20:02 2024 Cross-referenced by PHPXref