diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d727b17c..8a7997b3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,9 @@ updates: open-pull-requests-limit: 5 commit-message: prefix: "GH Actions:" + groups: + action-runners: + applies-to: version-updates + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index f30a48e9..070bec95 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -14,13 +14,14 @@ jobs: if: github.repository == 'PHPMailer/PHPMailer' steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 + persist-credentials: false - name: Build Docs uses: ./.github/actions/build-docs - name: Publish Docs to gh-pages - uses: JamesIves/github-pages-deploy-action@v4 + uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3 with: branch: gh-pages folder: docs diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 06fa35ac..189de394 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -7,8 +7,7 @@ on: push: branches: [ "master" ] -# Declare default permissions as read only. -permissions: read-all +permissions: {} jobs: analysis: @@ -17,23 +16,20 @@ jobs: name: Scorecards analysis runs-on: ubuntu-latest + permissions: - # Needed to upload the results to code-scanning dashboard. + # Required when publishing results (badge / API / code scanning) security-events: write - # Used to receive a badge. (Upcoming feature) id-token: write - # Needs for private repositories. - contents: read - actions: read - + steps: - name: "Checkout code" - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif @@ -52,7 +48,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -60,6 +56,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: sarif_file: results.sarif diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ec6e7ddb..30ba6233 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,8 +6,7 @@ on: # Allow manually triggering the workflow. workflow_dispatch: -permissions: - contents: read # to fetch code (actions/checkout) +permissions: {} jobs: @@ -15,12 +14,17 @@ jobs: runs-on: ubuntu-22.04 name: Coding standards + permissions: + contents: read # to fetch code (actions/checkout) + steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false - name: Set up PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 with: php-version: 'latest' coverage: none @@ -29,7 +33,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -55,12 +59,17 @@ jobs: name: "Lint: PHP ${{ matrix.php }}" continue-on-error: ${{ matrix.experimental }} + permissions: + contents: read # to fetch code (actions/checkout) + steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 with: php-version: ${{ matrix.php }} ini-values: error_reporting=-1, display_errors=On, display_startup_errors=On @@ -70,7 +79,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -125,9 +134,14 @@ jobs: continue-on-error: ${{ matrix.experimental }} + permissions: + contents: read # to fetch code (actions/checkout) + steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false # About the "extensions": # @@ -157,7 +171,7 @@ jobs: fi - name: Set up PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 with: php-version: ${{ matrix.php }} coverage: ${{ matrix.coverage && 'xdebug' || 'none' }} @@ -168,7 +182,7 @@ jobs: # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install PHP packages - normal if: ${{ matrix.php != '8.5' }} - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1 with: composer-options: ${{ steps.set_extensions.outputs.COMPOSER_OPTIONS }} # Bust the cache at least once a month - output format: YYYY-MM. @@ -176,7 +190,7 @@ jobs: - name: Install PHP packages - ignore-platform-reqs if: ${{ matrix.php == '8.5' }} - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1 with: composer-options: --ignore-platform-reqs ${{ steps.set_extensions.outputs.COMPOSER_OPTIONS }} # Bust the cache at least once a month - output format: YYYY-MM. @@ -185,7 +199,7 @@ jobs: # Install postfix and automatically retry if the install failed, which happens reguarly. # @link https://github.com/marketplace/actions/retry-step - name: Install postfix - uses: nick-invision/retry@v3 + uses: nick-invision/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 with: timeout_minutes: 2 max_attempts: 3 @@ -214,7 +228,7 @@ jobs: - name: Send coverage report to Codecov if: ${{ success() && matrix.coverage == true }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: diff --git a/changelog.md b/changelog.md index 272c73be..dd585eb9 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,12 @@ * Add support for [RFC4954](https://www.rfc-editor.org/rfc/rfc4954#section-4) two-part authentication for large XOAUTH2 tokens. * Also support empty tokens. * Avoid bogus static analyser deprecation warnings in `setFrom`. +* Make language loading entirely static, thanks to @SirLouen. +* Emit warnings when `parseAddresses()` is used without IMAP extension. +* Fix PHP 8.5 linting issue. +* Don't use `-t` switch when calling qmail. +* Checking for interrupted system calls now works in languages other than English. +* Add support for extracting gmail transaction IDs after sending. ## Version 6.10.0 (April 24th, 2025) * Add support for [RFC 6530 SMTPUTF8](https://www.rfc-editor.org/rfc/rfc6530), permitting use of UTF-8 Unicode characters everywhere, thanks to @arnt and ICANN. See `SMTPUTF8.md` for details. diff --git a/composer.json b/composer.json index 82da669a..e4dd7ddd 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,8 @@ "league/oauth2-google": "Needed for Google XOAUTH2 authentication", "psr/log": "For optional PSR-3 debug logging", "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication", - "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "ext-imap": "Needed to support advanced email address parsing according to RFC822" }, "autoload": { "psr-4": { diff --git a/language/phpmailer.lang-es.php b/language/phpmailer.lang-es.php index 4e74bfb7..35ce5b8e 100644 --- a/language/phpmailer.lang-es.php +++ b/language/phpmailer.lang-es.php @@ -9,7 +9,7 @@ */ $PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.'; -$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP está afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.'; +$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP ha sido afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.'; $PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.'; $PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.'; $PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.'; @@ -18,7 +18,7 @@ $PHPMAILER_LANG['execute'] = 'Imposible ejecutar: '; $PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: '; $PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: '; $PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: '; -$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: '; +$PHPMAILER_LANG['from_failed'] = 'La siguiente dirección de remitente falló: '; $PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.'; $PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: '; $PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no válido'; @@ -34,3 +34,4 @@ $PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.'; $PHPMAILER_LANG['smtp_detail'] = 'Detalle: '; $PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; $PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: '; +$PHPMAILER_LANG['imap_recommended'] = 'No se recomienda usar el analizador de direcciones simplificado. Instala la extensión IMAP de PHP para un análisis RFC822 más completo.'; diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 426618c7..e2d1e5c6 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -36,7 +36,24 @@ + + + */language/phpmailer\.lang*\.php$ + + + + */test/Fixtures/LocalizationTest/phpmailer.lang-yz\.php + + + */test/Fixtures/LocalizationTest/phpmailer.lang-yz\.php + + diff --git a/src/PHPMailer.php b/src/PHPMailer.php index 49fce31e..0596d166 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -711,7 +711,7 @@ class PHPMailer * * @var array */ - protected $language = []; + protected static $language = []; /** * The number of errors encountered. @@ -1102,7 +1102,7 @@ class PHPMailer //At-sign is missing. $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $kind, $address ); @@ -1187,7 +1187,7 @@ class PHPMailer if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { $error_message = sprintf( '%s: %s', - $this->lang('Invalid recipient kind'), + self::lang('Invalid recipient kind'), $kind ); $this->setError($error_message); @@ -1201,7 +1201,7 @@ class PHPMailer if (!static::validateAddress($address)) { $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $kind, $address ); @@ -1280,40 +1280,61 @@ class PHPMailer } } else { //Use this simpler parser - $list = explode(',', $addrstr); - foreach ($list as $address) { - $address = trim($address); - //Is there a separate name part? - if (strpos($address, '<') === false) { - //No separate name, just use the whole thing - if (static::validateAddress($address)) { - $addresses[] = [ - 'name' => '', - 'address' => $address, - ]; - } - } else { - list($name, $email) = explode('<', $address); - $email = trim(str_replace('>', '', $email)); - $name = trim($name); - if (static::validateAddress($email)) { - //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled - //If this name is encoded, decode it - if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { - $origCharset = mb_internal_encoding(); - mb_internal_encoding($charset); - //Undo any RFC2047-encoded spaces-as-underscores - $name = str_replace('_', '=20', $name); - //Decode the name - $name = mb_decode_mimeheader($name); - mb_internal_encoding($origCharset); - } - $addresses[] = [ - //Remove any surrounding quotes and spaces from the name - 'name' => trim($name, '\'" '), - 'address' => $email, - ]; + $addresses = self::parseSimplerAddresses($addrstr, $charset); + } + + return $addresses; + } + + /** + * Parse a string containing one or more RFC822-style comma-separated email addresses + * with the form "display name
" into an array of name/address pairs. + * Uses a simpler parser that does not require the IMAP extension but doesnt support + * the full RFC822 spec. For full RFC822 support, use the PHP IMAP extension. + * + * @param string $addrstr The address list string + * @param string $charset The charset to use when decoding the address list string. + * + * @return array + */ + protected static function parseSimplerAddresses($addrstr, $charset) + { + // Emit a runtime notice to recommend using the IMAP extension for full RFC822 parsing + trigger_error(self::lang('imap_recommended'), E_USER_NOTICE); + + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + $name = trim($name); + if (static::validateAddress($email)) { + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + //If this name is encoded, decode it + if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $name = str_replace('_', '=20', $name); + //Decode the name + $name = mb_decode_mimeheader($name); + mb_internal_encoding($origCharset); } + $addresses[] = [ + //Remove any surrounding quotes and spaces from the name + 'name' => trim($name, '\'" '), + 'address' => $email, + ]; } } } @@ -1349,7 +1370,7 @@ class PHPMailer ) { $error_message = sprintf( '%s (From): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $address ); $this->setError($error_message); @@ -1605,7 +1626,7 @@ class PHPMailer && ini_get('mail.add_x_header') === '1' && stripos(PHP_OS, 'WIN') === 0 ) { - trigger_error($this->lang('buggy_php'), E_USER_WARNING); + trigger_error(self::lang('buggy_php'), E_USER_WARNING); } try { @@ -1635,7 +1656,7 @@ class PHPMailer call_user_func_array([$this, 'addAnAddress'], $params); } if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { - throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); + throw new Exception(self::lang('provide_address'), self::STOP_CRITICAL); } //Validate From, Sender, and ConfirmReadingTo addresses @@ -1652,7 +1673,7 @@ class PHPMailer if (!static::validateAddress($this->{$address_kind})) { $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $address_kind, $this->{$address_kind} ); @@ -1674,7 +1695,7 @@ class PHPMailer $this->setMessageType(); //Refuse to send an empty message unless we are specifically allowing it if (!$this->AllowEmpty && empty($this->Body)) { - throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + throw new Exception(self::lang('empty_message'), self::STOP_CRITICAL); } //Trim subject consistently @@ -1834,7 +1855,7 @@ class PHPMailer foreach ($this->SingleToArray as $toAddr) { $mail = @popen($sendmail, 'w'); if (!$mail) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } $this->edebug("To: {$toAddr}"); fwrite($mail, 'To: ' . $toAddr . "\n"); @@ -1842,25 +1863,27 @@ class PHPMailer fwrite($mail, $body); $result = pclose($mail); $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); - $this->doCallback( - ($result === 0), - [[$addrinfo['address'], $addrinfo['name']]], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); + foreach ($addrinfo as $addr) { + $this->doCallback( + ($result === 0), + [[$addr['address'], $addr['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + } $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } } else { $mail = @popen($sendmail, 'w'); if (!$mail) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } fwrite($mail, $header); fwrite($mail, $body); @@ -1877,7 +1900,7 @@ class PHPMailer ); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } @@ -2017,16 +2040,18 @@ class PHPMailer foreach ($toArr as $toAddr) { $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); - $this->doCallback( - $result, - [[$addrinfo['address'], $addrinfo['name']]], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); + foreach ($addrinfo as $addr) { + $this->doCallback( + $result, + [[$addr['address'], $addr['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + } } } else { $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); @@ -2036,7 +2061,7 @@ class PHPMailer ini_set('sendmail_from', $old_from); } if (!$result) { - throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); + throw new Exception(self::lang('instantiate'), self::STOP_CRITICAL); } return true; @@ -2122,12 +2147,12 @@ class PHPMailer $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $bad_rcpt = []; if (!$this->smtpConnect($this->SMTPOptions)) { - throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + throw new Exception(self::lang('smtp_connect_failed'), self::STOP_CRITICAL); } //If we have recipient addresses that need Unicode support, //but the server doesn't support it, stop here if ($this->UseSMTPUTF8 && !$this->smtp->getServerExt('SMTPUTF8')) { - throw new Exception($this->lang('no_smtputf8'), self::STOP_CRITICAL); + throw new Exception(self::lang('no_smtputf8'), self::STOP_CRITICAL); } //Sender already validated in preSend() if ('' === $this->Sender) { @@ -2139,7 +2164,7 @@ class PHPMailer $this->smtp->xclient($this->SMTPXClient); } if (!$this->smtp->mail($smtp_from)) { - $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + $this->setError(self::lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); } @@ -2161,7 +2186,7 @@ class PHPMailer //Only send the DATA command if we have viable recipients if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) { - throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); + throw new Exception(self::lang('data_not_accepted'), self::STOP_CRITICAL); } $smtp_transaction_id = $this->smtp->getLastTransactionID(); @@ -2192,7 +2217,7 @@ class PHPMailer foreach ($bad_rcpt as $bad) { $errstr .= $bad['to'] . ': ' . $bad['error']; } - throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE); + throw new Exception(self::lang('recipients_failed') . $errstr, self::STOP_CONTINUE); } return true; @@ -2246,7 +2271,7 @@ class PHPMailer $hostinfo ) ) { - $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry)); + $this->edebug(self::lang('invalid_hostentry') . ' ' . trim($hostentry)); //Not a valid host entry continue; } @@ -2258,7 +2283,7 @@ class PHPMailer //Check the host name is a valid name or IP address before trying to use it if (!static::isValidHost($hostinfo[2])) { - $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); + $this->edebug(self::lang('invalid_host') . ' ' . $hostinfo[2]); continue; } $prefix = ''; @@ -2278,7 +2303,7 @@ class PHPMailer if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled if (!$sslext) { - throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + throw new Exception(self::lang('extension_missing') . 'openssl', self::STOP_CRITICAL); } } $host = $hostinfo[2]; @@ -2330,7 +2355,7 @@ class PHPMailer $this->oauth ) ) { - throw new Exception($this->lang('authenticate')); + throw new Exception(self::lang('authenticate')); } return true; @@ -2380,7 +2405,7 @@ class PHPMailer * * @return bool Returns true if the requested language was loaded, false otherwise. */ - public function setLanguage($langcode = 'en', $lang_path = '') + public static function setLanguage($langcode = 'en', $lang_path = '') { //Backwards compatibility for renamed language codes $renamed_langcodes = [ @@ -2429,6 +2454,8 @@ class PHPMailer 'smtp_error' => 'SMTP server error: ', 'variable_set' => 'Cannot set or reset variable: ', 'no_smtputf8' => 'Server does not support SMTPUTF8 needed to send to Unicode addresses', + 'imap_recommended' => 'Using simplified address parser is not recommended. ' . + 'Install the PHP IMAP extension for full RFC822 parsing.', ]; if (empty($lang_path)) { //Calculate an absolute path so it can work if CWD is not here @@ -2495,7 +2522,7 @@ class PHPMailer } } } - $this->language = $PHPMAILER_LANG; + self::$language = $PHPMAILER_LANG; return $foundlang; //Returns false if language not found } @@ -2507,11 +2534,11 @@ class PHPMailer */ public function getTranslations() { - if (empty($this->language)) { - $this->setLanguage(); // Set the default language. + if (empty(self::$language)) { + self::setLanguage(); // Set the default language. } - return $this->language; + return self::$language; } /** @@ -2934,10 +2961,6 @@ class PHPMailer //Create unique IDs and preset boundaries $this->setBoundaries(); - if ($this->sign_key_file) { - $body .= $this->getMailMIME() . static::$LE; - } - $this->setWordWrap(); $bodyEncoding = $this->Encoding; @@ -2969,6 +2992,12 @@ class PHPMailer if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) { $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; } + + if ($this->sign_key_file) { + $this->Encoding = $bodyEncoding; + $body .= $this->getMailMIME() . static::$LE; + } + //Use this as a preamble in all multipart message types $mimepre = ''; switch ($this->message_type) { @@ -3150,12 +3179,12 @@ class PHPMailer if ($this->isError()) { $body = ''; if ($this->exceptions) { - throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + throw new Exception(self::lang('empty_message'), self::STOP_CRITICAL); } } elseif ($this->sign_key_file) { try { if (!defined('PKCS7_TEXT')) { - throw new Exception($this->lang('extension_missing') . 'openssl'); + throw new Exception(self::lang('extension_missing') . 'openssl'); } $file = tempnam(sys_get_temp_dir(), 'srcsign'); @@ -3193,7 +3222,7 @@ class PHPMailer $body = $parts[1]; } else { @unlink($signed); - throw new Exception($this->lang('signing') . openssl_error_string()); + throw new Exception(self::lang('signing') . openssl_error_string()); } } catch (Exception $exc) { $body = ''; @@ -3338,7 +3367,7 @@ class PHPMailer ) { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name @@ -3351,7 +3380,7 @@ class PHPMailer $name = $filename; } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } $this->attachment[] = [ @@ -3512,11 +3541,11 @@ class PHPMailer { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = file_get_contents($path); if (false === $file_buffer) { - throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = $this->encodeString($file_buffer, $encoding); @@ -3569,9 +3598,9 @@ class PHPMailer $encoded = $this->encodeQP($str); break; default: - $this->setError($this->lang('encoding') . $encoding); + $this->setError(self::lang('encoding') . $encoding); if ($this->exceptions) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } break; } @@ -3846,7 +3875,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } //Append to $attachment array @@ -3905,7 +3934,7 @@ class PHPMailer ) { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name @@ -3914,7 +3943,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); @@ -3980,7 +4009,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } //Append to $attachment array @@ -4237,7 +4266,7 @@ class PHPMailer } if (strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception($this->lang('invalid_header')); + throw new Exception(self::lang('invalid_header')); } return false; @@ -4261,15 +4290,15 @@ class PHPMailer if ('smtp' === $this->Mailer && null !== $this->smtp) { $lasterror = $this->smtp->getError(); if (!empty($lasterror['error'])) { - $msg .= ' ' . $this->lang('smtp_error') . $lasterror['error']; + $msg .= ' ' . self::lang('smtp_error') . $lasterror['error']; if (!empty($lasterror['detail'])) { - $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail']; + $msg .= ' ' . self::lang('smtp_detail') . $lasterror['detail']; } if (!empty($lasterror['smtp_code'])) { - $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code']; + $msg .= ' ' . self::lang('smtp_code') . $lasterror['smtp_code']; } if (!empty($lasterror['smtp_code_ex'])) { - $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex']; + $msg .= ' ' . self::lang('smtp_code_ex') . $lasterror['smtp_code_ex']; } } } @@ -4394,21 +4423,21 @@ class PHPMailer * * @return string */ - protected function lang($key) + protected static function lang($key) { - if (count($this->language) < 1) { - $this->setLanguage(); //Set the default language + if (count(self::$language) < 1) { + self::setLanguage(); //Set the default language } - if (array_key_exists($key, $this->language)) { + if (array_key_exists($key, self::$language)) { if ('smtp_connect_failed' === $key) { //Include a link to troubleshooting docs on SMTP connection failure. //This is by far the biggest cause of support questions //but it's usually not PHPMailer's fault. - return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + return self::$language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; } - return $this->language[$key]; + return self::$language[$key]; } //Return the key as a fallback @@ -4423,7 +4452,7 @@ class PHPMailer */ private function getSmtpErrorMessage($base_key) { - $message = $this->lang($base_key); + $message = self::lang($base_key); $error = $this->smtp->getError(); if (!empty($error['error'])) { $message .= ' ' . $error['error']; @@ -4467,7 +4496,7 @@ class PHPMailer //Ensure name is not empty, and that neither name nor value contain line breaks if (empty($name) || strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception($this->lang('invalid_header')); + throw new Exception(self::lang('invalid_header')); } return false; @@ -4860,7 +4889,7 @@ class PHPMailer return true; } - $this->setError($this->lang('variable_set') . $name); + $this->setError(self::lang('variable_set') . $name); return false; } @@ -4998,7 +5027,7 @@ class PHPMailer { if (!defined('PKCS7_TEXT')) { if ($this->exceptions) { - throw new Exception($this->lang('extension_missing') . 'openssl'); + throw new Exception(self::lang('extension_missing') . 'openssl'); } return ''; diff --git a/src/SMTP.php b/src/SMTP.php index e6170f71..7c359cf7 100644 --- a/src/SMTP.php +++ b/src/SMTP.php @@ -205,6 +205,7 @@ class SMTP 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', 'ZoneMTA' => '/[\d]{3} Message queued as (.*)/', 'Mailjet' => '/[\d]{3} OK queued as (.*)/', + 'Gsmtp' => '/[\d]{3} 2\.0\.0 OK (.*) - gsmtp/', ]; /** @@ -1340,7 +1341,16 @@ class SMTP //stream_select returns false when the `select` system call is interrupted //by an incoming signal, try the select again - if (stripos($message, 'interrupted system call') !== false) { + if ( + stripos($message, 'interrupted system call') !== false || + ( + // on applications with a different locale than english, the message above is not found because + // it's translated. So we also check for the SOCKET_EINTR constant which is defined under + // Windows and UNIX-like platforms (if available on the platform). + defined('SOCKET_EINTR') && + stripos($message, 'stream_select(): Unable to select [' . SOCKET_EINTR . ']') !== false + ) + ) { $this->edebug( 'SMTP -> get_lines(): retrying stream_select', self::DEBUG_LOWLEVEL diff --git a/test/Fixtures/HasLineLongerThanMaxTest/cert.pem b/test/Fixtures/HasLineLongerThanMaxTest/cert.pem new file mode 100644 index 00000000..e7c90b30 --- /dev/null +++ b/test/Fixtures/HasLineLongerThanMaxTest/cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIUPVCD/ME/tR7lrcNY0eLMignHqywwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTA5MTExOTI4MDdaFw0zNTA5 +MDkxOTI4MDdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDHDtSUM73CqVfUt5hhBIbz56ENE746CqgqCqYpKypQ +frCDbcaRWagg9JOy4k9BChB4/B8wZilF9vsmfFoIa0H+LmQWQLN1pVx2tuSWI9rw +CdmTm6cXFZCxleOQMFxzmzV53gK9y2YRxAYL/hm6mcWp6Rblv0SqyxBz+GPJLLrr +cVRIgkktEia7ENA56DWpLoi49xYUwnDN3o+PwtrPGEzwsH/25zhEyS1LlcfRM3pY +W9UGX8HtU1LB94dWoVWNvISFvjicCWhVsuNw1Z0tIko499iEQG+zezbmh++n9a2G +bkCaI6dFZL5pHakmKOTYKyZ1sprE4799KDSTd8hlPfHboC4ClWqIiI6ou3kEpJln +sdsNZP5vPHrDgjuW/oE+zsQjmkaJiWaZphpthyYkR32Xu7HPvtQT4MHfkrs/SFE0 +43ml8CqrGSa+IjSjI+HXMwsf0mRmEtK7PcqVLhdSWAGjMNPjJ+er/O+PX3ZAWrbl +GzJfYU5LAk1ES/8uKpB+TjAXDL8xyM0+aP0axEeU57SyTNqbVfimrA250KZ+Q3hk +dpTWlTEjCXhxGHXdiJJwFPyanCNstFuKgNHTbmdRTMKIQ+Wmu5EgUSH2GcRZg9oO +t2veQP3EIc9dIxzijUFETWuBqzi80D6rKJJ1KowJE0rdh7owI/SCHNOYgjSN5a0G +XQIDAQABo1MwUTAdBgNVHQ4EFgQUzSwRCSiJnYQsy99FkcsdWzHJjIwwHwYDVR0j +BBgwFoAUzSwRCSiJnYQsy99FkcsdWzHJjIwwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAVZApbeRypzpwv2d8B/kPcIRcq5Kot0HhTDr9CNvGqU0G +TwQrVyIVAzi0uX+Ki7flj3+bo1br9xR/ocKbnTbEA3ofCxEbf0KGEjiwvB7tAg22 +UeFBxdAZG2IJcwwmY779IHKmjmFgrWGbXTirrN2a3i5TYU/nrTp7yY3GFQFujt5q +hQXBnkEvubS3n9ImdA0ByWCgmYiS08v8HGgsgGs9xVe1idkDkD+5I1imCADvUh7I +0ZksoB/XpdHRaqTRF0h6G2EUXznOG7x04uG4tiHkim1W4IkBBVTLxp6iul9n8GAe +QoZadHGaPIeytwl7A986Qo78WIltxZC+SBjJeQJG7/qHt/MvB8dBXZ49zg1SmHeV +ZtBWdtC0LBGcLoImm9m7DCyA9xMqSKSoOqmzXTlWcKQnPi3MeI5dqfWzuvk9LyLg +71hXXF4EnTgZpHw1ZWJBI47jEfsH2G0c7X46HPYjD4XcDCChNG81d0xQpZMbq5J7 +jy6PSbE/iGghEOuiF1NpQsrAnlf0UAzA27bUPyX0NFOmQmAejc8b6NqIljSQE/Xm +MOZlE4RpIa63EzzDo1fas8hhUhtz3loYysHN+nmw4N0BRjVenxatJXiunIbsDkzJ +VeBTrddQlqszd0qOlGCpJM0uhHuVDPntHKmFLo7O32aH2TgvYakpp54Xd+4xIqM= +-----END CERTIFICATE----- diff --git a/test/Fixtures/HasLineLongerThanMaxTest/key.pem b/test/Fixtures/HasLineLongerThanMaxTest/key.pem new file mode 100644 index 00000000..6d7ec066 --- /dev/null +++ b/test/Fixtures/HasLineLongerThanMaxTest/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +ujKpHkhiv0V4kA8yZpxQDcXp84JE +-----END PRIVATE KEY----- diff --git a/test/Fixtures/LocalizationTest/phpmailer.lang-yy.php b/test/Fixtures/LocalizationTest/phpmailer.lang-yy.php index eb485dbf..d3af6be6 100644 --- a/test/Fixtures/LocalizationTest/phpmailer.lang-yy.php +++ b/test/Fixtures/LocalizationTest/phpmailer.lang-yy.php @@ -12,6 +12,5 @@ echo $composer; $PHPMAILER_LANG['extension_missing'] = 'Confirming that test fixture was loaded correctly (yy).'; $PHPMAILER_LANG['empty_message'] = $composer; -$PHPMAILER_LANG['encoding'] = `ls -l`; $PHPMAILER_LANG['execute'] = exec('some harmful command'); $PHPMAILER_LANG['signing'] = "Double quoted but not interpolated $composer"; diff --git a/test/Fixtures/LocalizationTest/phpmailer.lang-yz.php b/test/Fixtures/LocalizationTest/phpmailer.lang-yz.php new file mode 100644 index 00000000..2b0f8aa3 --- /dev/null +++ b/test/Fixtures/LocalizationTest/phpmailer.lang-yz.php @@ -0,0 +1,14 @@ +getProperty('oauth'); - $property->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $property->setAccessible(true); $property->setValue($PHPMailer, true); self::assertTrue($PHPMailer->getOAuth(), 'Initial value of oauth property is not true'); diff --git a/test/PHPMailer/FileIsAccessibleTest.php b/test/PHPMailer/FileIsAccessibleTest.php index 8989643a..45f0837d 100644 --- a/test/PHPMailer/FileIsAccessibleTest.php +++ b/test/PHPMailer/FileIsAccessibleTest.php @@ -39,9 +39,9 @@ final class FileIsAccessibleTest extends TestCase public function testFileIsAccessible($input, $expected) { $reflMethod = new ReflectionMethod(PHPMailer::class, 'fileIsAccessible'); - $reflMethod->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); $result = $reflMethod->invoke(null, $input); - $reflMethod->setAccessible(false); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); self::assertSame($expected, $result); } @@ -91,9 +91,9 @@ final class FileIsAccessibleTest extends TestCase chmod($file, octdec('0')); $reflMethod = new ReflectionMethod(PHPMailer::class, 'fileIsAccessible'); - $reflMethod->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); $result = $reflMethod->invoke(null, $file); - $reflMethod->setAccessible(false); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); // Reset to the default for git files before running assertions. chmod($file, octdec('644')); diff --git a/test/PHPMailer/HasLineLongerThanMaxTest.php b/test/PHPMailer/HasLineLongerThanMaxTest.php index af6118d6..7fffee6d 100644 --- a/test/PHPMailer/HasLineLongerThanMaxTest.php +++ b/test/PHPMailer/HasLineLongerThanMaxTest.php @@ -56,6 +56,46 @@ final class HasLineLongerThanMaxTest extends PreSendTestCase ); } + /** + * Test constructing a SMIME signed message that contains lines that are too long for RFC compliance. + * + * @covers \PHPMailer\PHPMailer\PHPMailer::hasLineLongerThanMax + */ + public function testLongBodySmime() + { + $oklen = str_repeat(str_repeat('0', PHPMailer::MAX_LINE_LENGTH) . PHPMailer::getLE(), 2); + // Use +2 to ensure line length is over limit - LE may only be 1 char. + $badlen = str_repeat(str_repeat('1', PHPMailer::MAX_LINE_LENGTH + 2) . PHPMailer::getLE(), 2); + + $this->Mail->Body = 'This message contains lines that are too long.' . + PHPMailer::getLE() . $oklen . $badlen . $oklen; + self::assertTrue( + PHPMailer::hasLineLongerThanMax($this->Mail->Body), + 'Test content does not contain long lines!' + ); + + $this->Mail->isHTML(); + $this->buildBody(); + #$this->Mail->AltBody = $this->Mail->Body; + $this->Mail->Encoding = '8bit'; + $this->Mail->sign( + __DIR__ . '/../Fixtures/HasLineLongerThanMaxTest/cert.pem', + __DIR__ . '/../Fixtures/HasLineLongerThanMaxTest/key.pem', + null + ); + $this->Mail->preSend(); + $message = $this->Mail->getSentMIMEMessage(); + self::assertFalse( + PHPMailer::hasLineLongerThanMax($message), + 'Long line not corrected (Max: ' . (PHPMailer::MAX_LINE_LENGTH + strlen(PHPMailer::getLE())) . ' chars)' + ); + self::assertStringContainsString( + 'Content-Transfer-Encoding: quoted-printable', + $message, + 'Long line did not cause transfer encoding switch.' + ); + } + /** * Test constructing a message that does NOT contain lines that are too long for RFC compliance. * diff --git a/test/PHPMailer/IsPermittedPathTest.php b/test/PHPMailer/IsPermittedPathTest.php index 93823a44..f3f17537 100644 --- a/test/PHPMailer/IsPermittedPathTest.php +++ b/test/PHPMailer/IsPermittedPathTest.php @@ -35,9 +35,9 @@ final class IsPermittedPathTest extends TestCase public function testIsPermittedPath($input, $expected) { $reflMethod = new ReflectionMethod(PHPMailer::class, 'isPermittedPath'); - $reflMethod->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); $result = $reflMethod->invoke(null, $input); - $reflMethod->setAccessible(false); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); self::assertSame($expected, $result); } diff --git a/test/PHPMailer/LocalizationTest.php b/test/PHPMailer/LocalizationTest.php index c67ce578..4af752a3 100644 --- a/test/PHPMailer/LocalizationTest.php +++ b/test/PHPMailer/LocalizationTest.php @@ -15,6 +15,7 @@ namespace PHPMailer\Test\PHPMailer; use ReflectionMethod; use PHPMailer\Test\TestCase; +use PHPMailer\PHPMailer\PHPMailer; /** * Test localized error message functionality. @@ -305,13 +306,6 @@ final class LocalizationTest extends TestCase 'The "empty_message" translation is not as expected' ); - self::assertArrayHasKey('encoding', $lang, 'The "encoding" translation key was not found'); - self::assertSame( - 'Unknown encoding: ', - $lang['encoding'], - 'The "encoding" translation is not as expected' - ); - self::assertArrayHasKey('execute', $lang, 'The "execute" translation key was not found'); self::assertSame( 'Could not execute: ', @@ -327,6 +321,37 @@ final class LocalizationTest extends TestCase ); } + /** + * Test that arbitrary code in a language file does not get executed. + */ + public function testSetLanguageDoesNotExecuteCodeWithBackticksInLangFile() + { + $result = $this->Mail->setLanguage( + 'yz', // Unassigned lang code. + dirname(__DIR__) . '/Fixtures/LocalizationTest/' + ); + $lang = $this->Mail->getTranslations(); + + self::assertTrue($result, 'Setting the language failed. Translations set to: ' . var_export($lang, true)); + self::assertIsArray($lang, 'Translations is not an array'); + + // Verify that the fixture file was loaded. + self::assertArrayHasKey('extension_missing', $lang, 'The "extension_missing" translation key was not found'); + self::assertSame( + 'Confirming that test fixture was loaded correctly (yz).', + $lang['extension_missing'], + 'The "extension_missing" translation is not as expected' + ); + + // Verify that arbitrary code in a translation file does not get processed. + self::assertArrayHasKey('encoding', $lang, 'The "encoding" translation key was not found'); + self::assertSame( + 'Unknown encoding: ', + $lang['encoding'], + 'The "encoding" translation is not as expected' + ); + } + /** * Test that text strings passed in from a language file for arbitrary keys do not get processed. */ @@ -419,13 +444,13 @@ final class LocalizationTest extends TestCase public function testLang($input, $expected, $langCode = null) { if (isset($langCode)) { - $this->Mail->setLanguage($langCode); + PHPMailer::setLanguage($langCode); } - $reflMethod = new ReflectionMethod($this->Mail, 'lang'); - $reflMethod->setAccessible(true); - $result = $reflMethod->invoke($this->Mail, $input); - $reflMethod->setAccessible(false); + $reflMethod = new ReflectionMethod(PHPMailer::class, 'lang'); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); + $result = $reflMethod->invoke(null, $input); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); self::assertSame($expected, $result); } diff --git a/test/PHPMailer/PHPMailerTest.php b/test/PHPMailer/PHPMailerTest.php index 3c835480..cee80dd3 100644 --- a/test/PHPMailer/PHPMailerTest.php +++ b/test/PHPMailer/PHPMailerTest.php @@ -253,7 +253,7 @@ EOT; $PHPMailer = new PHPMailer(); $reflection = new \ReflectionClass($PHPMailer); $property = $reflection->getProperty('message_type'); - $property->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $property->setAccessible(true); $property->setValue($PHPMailer, 'inline'); self::assertIsString($PHPMailer->createBody()); diff --git a/test/PHPMailer/ParseAddressesTest.php b/test/PHPMailer/ParseAddressesTest.php index f6188b90..ee9064f1 100644 --- a/test/PHPMailer/ParseAddressesTest.php +++ b/test/PHPMailer/ParseAddressesTest.php @@ -14,6 +14,7 @@ namespace PHPMailer\Test\PHPMailer; use PHPMailer\PHPMailer\PHPMailer; +use ReflectionMethod; use Yoast\PHPUnitPolyfills\TestCases\TestCase; /** @@ -28,36 +29,6 @@ use Yoast\PHPUnitPolyfills\TestCases\TestCase; */ final class ParseAddressesTest extends TestCase { - /** - * Test RFC822 address splitting using the PHPMailer native implementation - * with the Mbstring extension available. - * - * @requires extension mbstring - * - * @dataProvider dataAddressSplitting - * - * @param string $addrstr The address list string. - * @param array $expected The expected function output. - * @param string $charset Optional. The charset to use. - */ - public function testAddressSplittingNative($addrstr, $expected, $charset = null) - { - if (isset($charset)) { - $parsed = PHPMailer::parseAddresses($addrstr, false, $charset); - } else { - $parsed = PHPMailer::parseAddresses($addrstr, false); - } - - $expectedOutput = $expected['default']; - if (empty($expected['native+mbstring']) === false) { - $expectedOutput = $expected['native+mbstring']; - } elseif (empty($expected['native']) === false) { - $expectedOutput = $expected['native']; - } - - $this->verifyExpectations($parsed, $expectedOutput); - } - /** * Test RFC822 address splitting using the IMAP implementation * with the Mbstring extension available. @@ -89,38 +60,6 @@ final class ParseAddressesTest extends TestCase $this->verifyExpectations($parsed, $expectedOutput); } - /** - * Test RFC822 address splitting using the PHPMailer native implementation - * without the Mbstring extension. - * - * @dataProvider dataAddressSplitting - * - * @param string $addrstr The address list string. - * @param array $expected The expected function output. - * @param string $charset Optional. The charset to use. - */ - public function testAddressSplittingNativeNoMbstring($addrstr, $expected, $charset = null) - { - if (extension_loaded('mbstring')) { - self::markTestSkipped('Test requires MbString *not* to be available'); - } - - if (isset($charset)) { - $parsed = PHPMailer::parseAddresses($addrstr, false, $charset); - } else { - $parsed = PHPMailer::parseAddresses($addrstr, false); - } - - $expectedOutput = $expected['default']; - if (empty($expected['native--mbstring']) === false) { - $expectedOutput = $expected['native--mbstring']; - } elseif (empty($expected['native']) === false) { - $expectedOutput = $expected['native']; - } - - $this->verifyExpectations($parsed, $expectedOutput); - } - /** * Test RFC822 address splitting using the IMAP implementation * without the Mbstring extension. @@ -155,6 +94,52 @@ final class ParseAddressesTest extends TestCase $this->verifyExpectations($parsed, $expectedOutput); } + /** + * Test RFC822 address splitting using the native implementation + * + * @dataProvider dataAddressSplittingNative + * + * @param string $addrstr The address list string. + * @param array $expected The expected function output. + * @param string $charset Optional.The charset to use. + */ + public function testAddressSplittingNative($addrstr, $expected, $charset = PHPMailer::CHARSET_ISO88591) + { + error_reporting(E_ALL & ~E_USER_NOTICE); + $reflMethod = new ReflectionMethod(PHPMailer::class, 'parseSimplerAddresses'); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); + $parsed = $reflMethod->invoke(null, $addrstr, $charset); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); + $this->verifyExpectations($parsed, $expected); + } + + /** + * Data provider for testAddressSplittingNative. + * + * @return array + * addrstr: string, + * expected: array{name: string, address: string}[] + * charset: string + */ + public function dataAddressSplittingNative() + { + return [ + 'Valid address: single address without name' => [ + 'addrstr' => 'joe@example.com', + 'expected' => [ + ['name' => '', 'address' => 'joe@example.com'], + ], + ], + 'Valid address: two addresses with names' => [ + 'addrstr' => 'Joe User , Jill User ', + 'expected' => [ + ['name' => 'Joe User', 'address' => 'joe@example.com'], + ['name' => 'Jill User', 'address' => 'jill@example.net'], + ], + ], + ]; + } + /** * Verify the expectations. * diff --git a/test/TestCase.php b/test/TestCase.php index 40fbc8e5..c9ec7a26 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -119,6 +119,7 @@ abstract class TestCase extends PolyfillTestCase private $PHPMailerStaticProps = [ 'LE' => PHPMailer::CRLF, 'validator' => 'php', + 'language' => [], ]; /**