Commit 0a07391e71 for wordpress.org

commit 0a07391e71e550212c70252ed9f99dcf61a98a5c
Author: SergeyBiryukov <sergeybiryukov@git.wordpress.org>
Date:   Thu Nov 12 23:20:05 2026 +0000

    External Libraries: Upgrade PHPMailer to version 7.2.0.

    Changes in the latest version:

    - **XOAUTH:** authentication with a long token could report success when it had actually failed.
    - **S/MIME:** signed messages no longer contain bare LF line breaks, which some SMTP servers reject.
    - **Long address headers:** these are now folded to stay within the RFC 2822 line length limit.
    - **`QUIT`:** no longer sent while a `DATA` command is still in progress.
    - **XCLIENT:**
      - Attribute values are now xtext-encoded when they're sent, which prevents attribute injection.
      - The expected reply is now 220, not 250, and `EHLO` is sent again afterwards.
      - Invalid attributes are rejected.
    - **`DSNConfigurator`:** `smtps://` DSNs now use `ENCRYPTION_SMTPS` instead of `ENCRYPTION_STARTTLS`.
    - **Deprecations:** `PHPMailer::parseAddresses()` and the `pcre` and `pcre8` patterns for `validateAddress()` are deprecated, and will be removed in PHPMailer 8.0. The deprecation is only in the docblocks; no notice is triggered.

    WordPress core doesn't use XCLIENT, `DSNConfigurator`, `parseAddresses()` or `validateAddress()`. Plugins that call these directly may be affected.

    References:
    * [https://github.com/PHPMailer/PHPMailer/releases/tag/v7.2.0 PHPMailer 7.2.0 release notes]
    * [https://github.com/PHPMailer/PHPMailer/compare/v7.1.1...v7.2.0 Full list of changes in PHPMailer 7.2.0]

    Follow-up to r54937, r55557, r56484, r57137, r59246, r59481, r60623, r60813, r60888, r61249, r61468, r62975.

    Props desrosj.
    Fixes #66649.
    Built from https://develop.svn.wordpress.org/trunk@65457


    git-svn-id: http://core.svn.wordpress.org/trunk@64585 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/PHPMailer/DSNConfigurator.php b/wp-includes/PHPMailer/DSNConfigurator.php
index 7058c1f05e..9c855fa729 100644
--- a/wp-includes/PHPMailer/DSNConfigurator.php
+++ b/wp-includes/PHPMailer/DSNConfigurator.php
@@ -22,7 +22,10 @@
 namespace PHPMailer\PHPMailer;

 /**
- * Configure PHPMailer with DSN string.
+ * Configure PHPMailer with a DSN string.
+ * Be aware that a DSN string can configure *all* PHPMailer properties,
+ * including those that have security consequences, so do not use
+ * user-provided DSN strings without strict validation of all parameters.
  *
  * @see https://en.wikipedia.org/wiki/Data_source_name
  *
@@ -140,7 +143,7 @@ class DSNConfigurator
         $isSMTPS = 'smtps' === $config['scheme'];

         if ($isSMTPS) {
-            $mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
+            $mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
         }

         $mailer->Host = $config['host'];
diff --git a/wp-includes/PHPMailer/PHPMailer.php b/wp-includes/PHPMailer/PHPMailer.php
index 4900cbc43a..5fc3208f58 100644
--- a/wp-includes/PHPMailer/PHPMailer.php
+++ b/wp-includes/PHPMailer/PHPMailer.php
@@ -769,7 +769,7 @@ class PHPMailer
      *
      * @var string
      */
-    const VERSION = '7.1.1';
+    const VERSION = '7.2.0';

     /**
      * Error severity: message only, continue processing.
@@ -1288,6 +1288,7 @@ class PHPMailer
      * the deprecated $useimap argument is truthy.
      * Note that quotes in the name part are removed.
      *
+     * @deprecated This method will be removed in the next major release of PHPMailer.
      * @see https://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
      *
      * @param string $addrstr The address list string
@@ -1300,11 +1301,11 @@ class PHPMailer
      */
     public static function parseAddresses($addrstr, $useimap = null, $charset = self::CHARSET_ISO88591)
     {
-        if ($useimap == true) {
+        if ($useimap) {
             trigger_error(self::lang('deprecated_argument') . '$useimap', E_USER_DEPRECATED);
         }
         $addresses = [];
-        if ($useimap == true && function_exists('imap_rfc822_parse_adrlist')) {
+        if ($useimap !== false && function_exists('imap_rfc822_parse_adrlist')) {
             //Use this built-in parser if it's available
             // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.imap_rfc822_parse_adrlistRemoved -- wrapped in function_exists()
             $list = imap_rfc822_parse_adrlist($addrstr, '');
@@ -1486,8 +1487,8 @@ class PHPMailer
      * Check that a string looks like an email address.
      * Validation patterns supported:
      * * `auto` Pick best pattern automatically;
-     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
-     * * `pcre` Use old PCRE implementation;
+     * * `pcre8` (deprecated, will convert to PHP validator) Use the squiloople.com pattern, requires PCRE > 8.0
+     * * `pcre` (deprecated, will convert to PHP validator) Use old PCRE implementation, kept for BC;
      * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
      * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
      * * `eai` Use a pattern similar to the HTML5 spec for 'email' and to firefox, extended to support EAI (RFC6530).
@@ -1503,7 +1504,8 @@ class PHPMailer
      * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
      *
      * @param string          $address       The email address to check
-     * @param string|callable $patternselect Which pattern to use
+     * @param string|callable $patternselect Which pattern to use;
+     *                                        the `pcre` and `pcre8` patterns are deprecated
      *
      * @return bool
      */
@@ -1521,7 +1523,7 @@ class PHPMailer
             return false;
         }
         switch ($patternselect) {
-            case 'pcre': //Kept for BC
+            case 'pcre': //Deprecated, kept for BC
             case 'pcre8':
                 /*
                  * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
@@ -2191,12 +2193,17 @@ class PHPMailer
      */
     public function setSMTPXclientAttribute($name, $value)
     {
-        if (!in_array($name, SMTP::$xclient_allowed_attributes)) {
+        if (!in_array($name, SMTP::$xclient_allowed_attributes, true)) {
             return false;
         }
         if (isset($this->SMTPXClient[$name]) && $value === null) {
             unset($this->SMTPXClient[$name]);
         } elseif ($value !== null) {
+            if (!is_string($value)) {
+                //Reject invalid XCLIENT attributes
+                return false;
+            }
+            //Values are stored raw and xtext-encoded at send time
             $this->SMTPXClient[$name] = $value;
         }

@@ -2247,7 +2254,17 @@ class PHPMailer
             $smtp_from = $this->Sender;
         }
         if (count($this->SMTPXClient)) {
-            $this->smtp->xclient($this->SMTPXClient);
+            if (!$this->smtp->xclient($this->SMTPXClient)) {
+                throw new Exception($this->smtp->getError()['error'], self::STOP_CRITICAL);
+            }
+            //XCLIENT success resets the session to the greeting protocol stage,
+            //so the client must send EHLO again before starting a transaction.
+            if ($this->Helo) {
+                $hello = $this->Helo;
+            } else {
+                $hello = $this->serverHostname();
+            }
+            $this->smtp->hello($hello);
         }
         if (!$this->smtp->mail($smtp_from)) {
             $this->setError(self::lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
@@ -2646,7 +2663,22 @@ class PHPMailer
             $addresses[] = $this->addrFormat($address);
         }

-        return $type . ': ' . implode(', ', $addresses) . static::$LE;
+        //Fold between addresses so that no line exceeds the RFC 2822 line length limit
+        $header = $type . ': ' . array_shift($addresses);
+        $lineLength = strlen($header);
+        foreach ($addresses as $address) {
+            //The address goes after ', ' and must still leave room for the ',' that ends
+            //the line if the next address gets folded
+            if ($lineLength + strlen(', ') + strlen($address) + strlen(',') > static::MAX_LINE_LENGTH) {
+                $header .= ',' . static::$LE . ' ' . $address;
+                $lineLength = strlen(' ') + strlen($address);
+            } else {
+                $header .= ', ' . $address;
+                $lineLength += strlen(', ') + strlen($address);
+            }
+        }
+
+        return $header . static::$LE;
     }

     /**
@@ -3319,8 +3351,9 @@ class PHPMailer
                     @unlink($signed);
                     //The message returned by openssl contains both headers and body, so need to split them up
                     $parts = explode("\n\n", $body, 2);
-                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
-                    $body = $parts[1];
+                    //openssl emits bare LF breaks, which SMTP servers may reject
+                    $this->MIMEHeader .= static::normalizeBreaks($parts[0]) . static::$LE . static::$LE;
+                    $body = static::normalizeBreaks($parts[1]);
                 } else {
                     @unlink($signed);
                     throw new Exception(self::lang('signing') . openssl_error_string());
diff --git a/wp-includes/PHPMailer/POP3.php b/wp-includes/PHPMailer/POP3.php
index 0ba9678373..823ec8be3d 100644
--- a/wp-includes/PHPMailer/POP3.php
+++ b/wp-includes/PHPMailer/POP3.php
@@ -47,7 +47,7 @@ class POP3
      * @var string
      * @deprecated This constant will be removed in PHPMailer 8.0. Use `PHPMailer::VERSION` instead.
      */
-    const VERSION = '7.1.1';
+    const VERSION = '7.2.0';

     /**
      * Default POP3 port number.
diff --git a/wp-includes/PHPMailer/SMTP.php b/wp-includes/PHPMailer/SMTP.php
index f0957b80a9..605d136b15 100644
--- a/wp-includes/PHPMailer/SMTP.php
+++ b/wp-includes/PHPMailer/SMTP.php
@@ -36,7 +36,7 @@ class SMTP
      * @var string
      * @deprecated This constant will be removed in PHPMailer 8.0. Use `PHPMailer::VERSION` instead.
      */
-    const VERSION = '7.1.1';
+    const VERSION = '7.2.0';

     /**
      * SMTP line break constant.
@@ -212,6 +212,7 @@ class SMTP
     /**
      * Allowed SMTP XCLIENT attributes.
      * Must be allowed by the SMTP server. EHLO response is not checked.
+     * This is a static rather than a constant because PHP 5.5 doesn't allow constant arrays.
      *
      * @see https://www.postfix.org/XCLIENT_README.html
      *
@@ -275,6 +276,14 @@ class SMTP
      */
     protected $last_reply = '';

+    /**
+     * The last command sent to the server.
+     * Used to avoid sending QUIT while a DATA command is still in progress.
+     *
+     * @var string
+     */
+    protected $state = '';
+
     /**
      * Output debugging info via a user-selected method.
      *
@@ -671,10 +680,11 @@ class SMTP
                     if (!$this->sendCommand('OAuth TOKEN', $oauth, [235, 334])) {
                         return false;
                     }
-                    //If the server answers with 334, send an empty line and wait for a 235
+                    //If the server answers with 334, send an empty line
+                    //a reply other than a 235 means the authentication failed, so we don't need to send anything else
                     if (
-                        substr($this->last_reply, 0, 3) === '334'
-                        && $this->sendCommand('AUTH End', '', 235)
+                        strpos($this->last_reply, '334') === 0
+                        && !$this->sendCommand('AUTH End', '', 235)
                     ) {
                         return false;
                     }
@@ -762,6 +772,7 @@ class SMTP
     {
         $this->server_caps = null;
         $this->helo_rply = null;
+        $this->state = '';
         if (is_resource($this->smtp_conn)) {
             //Close the connection and cleanup
             fclose($this->smtp_conn);
@@ -1020,7 +1031,12 @@ class SMTP
      */
     public function quit($close_on_error = true)
     {
-        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
+        if ($this->state === 'DATA') {
+            //The server would take QUIT as part of an unfinished message, so just close the connection
+            $noerror = true;
+        } else {
+            $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
+        }
         $err = $this->error; //Save any error
         if ($noerror || $close_on_error) {
             $this->close();
@@ -1070,8 +1086,39 @@ class SMTP
         );
     }

+    /**
+     * Encode a string using xtext encoding as defined in RFC 1891 & 3461,
+     * as required for XCLIENT attribute values: any octet outside
+     * printable US-ASCII (33-126), including '+', whitespace, and control
+     * characters, is encoded as '+' followed by two uppercase hexadecimal
+     * digits. Values encoded this way cannot inject additional attributes.
+     *
+     * @param string $value The string to encode
+     *
+     * @return string The xtext-encoded string
+     */
+    public static function xtext($value)
+    {
+        return preg_replace_callback(
+            '/[^\x21-\x7E]|\+/',
+            static function ($matches) {
+                return sprintf('+%02X', ord($matches[0]));
+            },
+            $value
+        );
+    }
+
     /**
      * Send SMTP XCLIENT command to server and check its return code.
+     * On success the server resets the session to the initial greeting
+     * protocol stage and responds with 220, so a new EHLO is required
+     * before any MAIL command.
+     *
+     * Attribute values are xtext-encoded (RFC 1891 & 3461) before being
+     * sent.
+     *
+     * @see https://www.postfix.org/XCLIENT_README.html
+     * @see https://www.rfc-editor.org/info/rfc3461/#section-4
      *
      * @return bool True on success
      */
@@ -1079,14 +1126,20 @@ class SMTP
     {
         $xclient_options = "";
         foreach ($vars as $key => $value) {
-            if (in_array($key, SMTP::$xclient_allowed_attributes)) {
-                $xclient_options .= " {$key}={$value}";
+            if (!in_array($key, SMTP::$xclient_allowed_attributes)) {
+                $this->setError("Invalid XCLIENT attribute name: $key");
+                return false;
+            }
+            if (!is_string($value)) {
+                $this->setError("XCLIENT attribute value for $key must be a string");
+                return false;
             }
+            $xclient_options .= " {$key}=" . static::xtext($value);
         }
         if (!$xclient_options) {
             return true;
         }
-        return $this->sendCommand('XCLIENT', 'XCLIENT' . $xclient_options, 250);
+        return $this->sendCommand('XCLIENT', 'XCLIENT' . $xclient_options, 220);
     }

     /**
@@ -1123,6 +1176,7 @@ class SMTP

             return false;
         }
+        $this->state = $command;
         $this->client_send($commandstring . static::LE, $command);

         $this->last_reply = $this->get_lines();
diff --git a/wp-includes/version.php b/wp-includes/version.php
index e4708922bb..5c7f5d9a29 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-beta5-65445';
+$wp_version = '7.2-beta5-65457';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.