Mail Parser: Completely remove Webklex IMAP and all dependcies

This commit is contained in:
johnnyq
2026-06-24 13:39:07 -04:00
parent 63ad3256ee
commit 171a0d38f8
779 changed files with 6408 additions and 82971 deletions

View File

@@ -181,7 +181,11 @@ final class Message
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
if ($messageParts === false || count($messageParts) !== 2) {
if ($messageParts === false) {
throw new \RuntimeException('Unable to split HTTP message: '.preg_last_error_msg());
}
if (count($messageParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
}
@@ -189,24 +193,48 @@ final class Message
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
if ($headerParts === false || count($headerParts) !== 2) {
if ($headerParts === false) {
throw new \RuntimeException('Unable to split HTTP message headers: '.preg_last_error_msg());
}
if (count($headerParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing status line');
}
[$startLine, $rawHeaders] = $headerParts;
if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
$versionMatch = preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches);
if ($versionMatch === false) {
throw new \RuntimeException('Unable to parse HTTP start line: '.preg_last_error_msg());
}
if ($versionMatch === 1 && $matches[1] === '1.0') {
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
$rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
if ($rawHeaders === null) {
throw new \RuntimeException('Unable to unfold HTTP headers: '.preg_last_error_msg());
}
}
/** @var array[] $headerLines */
$count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
if ($count === false) {
throw new \RuntimeException('Unable to parse HTTP headers: '.preg_last_error_msg());
}
// If these aren't the same, then one line didn't match and there's an invalid header.
if ($count !== substr_count($rawHeaders, "\n")) {
// Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
$hasFoldedHeader = preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders);
if ($hasFoldedHeader === false) {
throw new \RuntimeException('Unable to inspect HTTP header folding: '.preg_last_error_msg());
}
if ($hasFoldedHeader === 1) {
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
}
@@ -278,8 +306,18 @@ final class Message
public static function parseRequest(string $message): RequestInterface
{
$data = self::parseMessage($message);
if (strpbrk($data['start-line'], "\r\n") !== false) {
throw new \InvalidArgumentException('Invalid request string');
}
$matches = [];
if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
$requestStartLineMatch = preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches);
if ($requestStartLineMatch === false) {
throw new \RuntimeException('Unable to parse request start line: '.preg_last_error_msg());
}
if ($requestStartLineMatch === 0) {
throw new \InvalidArgumentException('Invalid request string');
}
$parts = explode(' ', $data['start-line'], 3);
@@ -304,10 +342,20 @@ final class Message
public static function parseResponse(string $message): ResponseInterface
{
$data = self::parseMessage($message);
if (strpbrk($data['start-line'], "\r\n") !== false) {
throw new \InvalidArgumentException('Invalid response string');
}
// According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
// the space between status-code and reason-phrase is required. But
// browsers accept responses without space and reason as well.
if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
$responseStartLineMatch = preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line']);
if ($responseStartLineMatch === false) {
throw new \RuntimeException('Unable to parse response start line: '.preg_last_error_msg());
}
if ($responseStartLineMatch === 0) {
throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']);
}
$parts = explode(' ', $data['start-line'], 3);

View File

@@ -43,6 +43,8 @@ trait MessageTrait
);
}
$this->assertProtocolVersion($version);
if ($this->protocol === $version) {
return $this;
}
@@ -273,6 +275,12 @@ trait MessageTrait
));
}
// Convert non-finite floats explicitly, as implicit coercion of
// NAN emits a warning on PHP 8.5.
if (is_float($value) && !is_finite($value)) {
$value = is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
}
$trimmed = trim((string) $value, " \t");
$this->assertValue($trimmed);
@@ -301,6 +309,23 @@ trait MessageTrait
}
}
/**
* @param mixed $version
*/
private function assertProtocolVersion($version): void
{
if (is_string($version)) {
$this->assertNoLineSeparators($version, 'Protocol version');
}
}
private function assertNoLineSeparators(string $value, string $field): void
{
if (strpbrk($value, "\r\n") !== false) {
throw new \InvalidArgumentException($field.' must not contain CR or LF characters.');
}
}
/**
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
*

View File

@@ -26,8 +26,9 @@ final class MultipartStream implements StreamInterface
* @param array $elements Array of associative arrays, each containing a
* required "name" key mapping to the form field,
* name, a required "contents" key mapping to any
* non-array value accepted by Utils::streamFor(),
* or an array for nested expansion.
* non-array value accepted by Utils::streamFor()
* (non-string scalar field values are cast to
* string), or an array for nested expansion.
* Optional keys include "headers" (associative
* array of custom headers) and "filename" (string
* to send as the filename in the part).
@@ -124,7 +125,27 @@ final class MultipartStream implements StreamInterface
return;
}
$element['contents'] = Utils::streamFor($element['contents']);
$contents = $element['contents'];
if (is_scalar($contents) && !is_string($contents)) {
// Multipart field values are byte strings on the wire, so finite
// numeric and boolean field values are cast to string here rather
// than tripping streamFor()'s non-string-scalar deprecation. Non-finite
// floats are deprecated and normalized here too, so the deprecation is
// reported against MultipartStream instead of transitively through
// streamFor().
if (is_float($contents) && !is_finite($contents)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.12',
'Passing a non-finite float as multipart contents is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
);
$contents = is_nan($contents) ? 'NAN' : ($contents > 0 ? 'INF' : '-INF');
}
$contents = (string) $contents;
}
$element['contents'] = Utils::streamFor($contents);
if (empty($element['filename'])) {
$uri = $element['contents']->getMetadata('uri');

View File

@@ -96,7 +96,7 @@ final class Query
$k = $encoder((string) $k);
if (!is_array($v)) {
$qs .= $k;
$v = is_bool($v) ? $castBool($v) : $v;
$v = is_bool($v) ? $castBool($v) : self::normalizeNonFiniteFloat($v);
if ($v !== null) {
$qs .= '='.$encoder((string) $v);
}
@@ -104,7 +104,7 @@ final class Query
} else {
foreach ($v as $vv) {
$qs .= $k;
$vv = is_bool($vv) ? $castBool($vv) : $vv;
$vv = is_bool($vv) ? $castBool($vv) : self::normalizeNonFiniteFloat($vv);
if ($vv !== null) {
$qs .= '='.$encoder((string) $vv);
}
@@ -115,4 +115,27 @@ final class Query
return $qs ? (string) substr($qs, 0, -1) : '';
}
/**
* Converts non-finite floats to the strings PHP coerces them to, as
* implicit coercion of NAN emits a warning on PHP 8.5.
*
* @param mixed $value
*
* @return mixed
*/
private static function normalizeNonFiniteFloat($value)
{
if (is_float($value) && !is_finite($value)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.12',
'Passing a non-finite float to Query::build() is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
);
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
}
return $value;
}
}

View File

@@ -40,6 +40,8 @@ class Request implements RequestInterface
string $version = '1.1'
) {
$this->assertMethod($method);
$this->assertProtocolVersion($version);
if (!$uri instanceof UriInterface) {
$uri = new Uri($uri);
}
@@ -78,7 +80,13 @@ class Request implements RequestInterface
public function withRequestTarget($requestTarget): RequestInterface
{
if (preg_match('#\s#', $requestTarget)) {
$hasWhitespace = preg_match('#\s#', $requestTarget);
if ($hasWhitespace === false) {
throw new \RuntimeException('Unable to validate request target: '.preg_last_error_msg());
}
if ($hasWhitespace === 1) {
throw new InvalidArgumentException(
'Invalid request target provided; cannot contain whitespace'
);
@@ -170,6 +178,8 @@ class Request implements RequestInterface
if (!is_string($method) || $method === '') {
throw new InvalidArgumentException('Method must be a non-empty string.');
}
$this->assertNoLineSeparators($method, 'Method');
}
private static function warnOnMethodCasingChange(string $method): void

View File

@@ -99,6 +99,7 @@ class Response implements ResponseInterface
?string $reason = null
) {
$this->assertStatusCodeRange($status);
$this->assertProtocolVersion($version);
$this->statusCode = $status;
@@ -108,11 +109,14 @@ class Response implements ResponseInterface
$this->setHeaders($headers);
if ($reason == '' && isset(self::PHRASES[$this->statusCode])) {
$this->reasonPhrase = self::PHRASES[$this->statusCode];
$reasonPhrase = self::PHRASES[$this->statusCode];
} else {
$this->reasonPhrase = (string) $reason;
$reasonPhrase = (string) $reason;
}
$this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
$this->reasonPhrase = $reasonPhrase;
$this->protocol = $version;
}
@@ -155,7 +159,9 @@ class Response implements ResponseInterface
if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) {
$reasonPhrase = self::PHRASES[$new->statusCode];
}
$new->reasonPhrase = (string) $reasonPhrase;
$reasonPhrase = (string) $reasonPhrase;
$this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
$new->reasonPhrase = $reasonPhrase;
return $new;
}

View File

@@ -68,7 +68,13 @@ final class Rfc7230
private static function isValidHostHeaderHost(string $host): bool
{
if (preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host)) {
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
if ($invalidHost === false) {
return false;
}
if ($invalidHost === 1) {
return false;
}

View File

@@ -101,10 +101,22 @@ class Uri implements UriInterface, \JsonSerializable
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4 tails.
$prefix = '';
if (preg_match('%^([0-9A-Za-z+.-]+://\[[0-9:.a-fA-F]+\])(.*?)$%', $url, $matches)) {
$ipv6Prefix = preg_match('%\A([0-9A-Za-z+.-]+://\[[^\]\x00-\x20/?#@]+\])(.*)\z%s', $url, $matches);
if ($ipv6Prefix === false) {
return false;
}
if ($ipv6Prefix === 1) {
/** @var array{0:string, 1:string, 2:string} $matches */
$suffix = $matches[2];
if ($suffix !== '' && strpos(':/?#', $suffix[0]) === false) {
return false;
}
$prefix = $matches[1];
$url = $matches[2];
$url = $suffix;
}
/** @var string|null */
@@ -371,12 +383,38 @@ class Uri implements UriInterface, \JsonSerializable
$result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
foreach ($keyValueArray as $key => $value) {
$result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null);
$result[] = self::generateQueryString((string) $key, $value !== null ? self::stringifyQueryValue($value) : null);
}
return $uri->withQuery(implode('&', $result));
}
/**
* Stringifies a non-null query value, deprecating non-string values that
* guzzlehttp/psr7 3.0 will reject. Non-finite floats are normalized to the
* strings PHP coerces them to, as implicit coercion of NAN emits a warning
* on PHP 8.5.
*
* @param mixed $value
*/
private static function stringifyQueryValue($value): string
{
if (!is_string($value)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.12',
'Passing %s to Uri::withQueryValues() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string or null query values.',
\gettype($value)
);
if (is_float($value) && !is_finite($value)) {
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
}
}
return (string) $value;
}
/**
* Creates a URI from a hash of `parse_url` components.
*
@@ -410,7 +448,27 @@ class Uri implements UriInterface, \JsonSerializable
return;
}
if (preg_match('/[\x00-\x20\x7F]/', $host)) {
// Reject control characters and URI authority delimiters so getHost()
// cannot disagree with the on-wire authority.
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
if ($invalidHost === false) {
throw new \RuntimeException('Unable to validate URI host: '.preg_last_error_msg());
}
if ($invalidHost === 1) {
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
}
if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
if ($host[0] !== '[' || substr($host, -1) !== ']') {
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
}
return;
}
if (strpos($host, ':') !== false) {
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
}
}
@@ -657,10 +715,10 @@ class Uri implements UriInterface, \JsonSerializable
throw new \InvalidArgumentException('User info must be a string');
}
return preg_replace_callback(
return $this->filterComponent(
'/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$component
$component,
'Unable to filter URI user info'
);
}
@@ -759,10 +817,10 @@ class Uri implements UriInterface, \JsonSerializable
throw new \InvalidArgumentException('Path must be a string');
}
return preg_replace_callback(
return $this->filterComponent(
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$path
$path,
'Unable to filter URI path'
);
}
@@ -779,13 +837,24 @@ class Uri implements UriInterface, \JsonSerializable
throw new \InvalidArgumentException('Query and fragment must be a string');
}
return preg_replace_callback(
return $this->filterComponent(
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$str
$str,
'Unable to filter URI query or fragment'
);
}
private function filterComponent(string $pattern, string $component, string $context): string
{
$filtered = preg_replace_callback($pattern, [$this, 'rawurlencodeMatchZero'], $component);
if ($filtered === null) {
throw new \RuntimeException($context.': '.preg_last_error_msg());
}
return $filtered;
}
private function rawurlencodeMatchZero(array $match): string
{
return rawurlencode($match[0]);

View File

@@ -150,7 +150,13 @@ final class UriNormalizer
}
if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
$uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath()));
$path = preg_replace('#//++#', '/', $uri->getPath());
if ($path === null) {
throw new \RuntimeException('Unable to remove duplicate slashes from URI path: '.preg_last_error_msg());
}
$uri = $uri->withPath($path);
}
if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') {
@@ -217,7 +223,7 @@ final class UriNormalizer
$normalized = preg_replace_callback($regex, $callback, $component);
if ($normalized === null) {
throw new \RuntimeException('Unable to normalize URI component percent-encoding');
throw new \RuntimeException('Unable to normalize URI component percent-encoding: '.preg_last_error_msg());
}
return $normalized;

View File

@@ -462,6 +462,9 @@ final class Utils
* in subsequent reads. String inputs are always treated as string bodies,
* even when they name callable functions.
*
* Passing a non-string scalar (`int`, `float`, or `bool`) is deprecated; cast
* it to a string instead. guzzlehttp/psr7 3.0 will reject non-string scalars.
*
* @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
* @param array{size?: int, metadata?: array} $options Additional options
*
@@ -470,6 +473,22 @@ final class Utils
public static function streamFor($resource = '', array $options = []): StreamInterface
{
if (is_scalar($resource)) {
if (!is_string($resource)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.12',
'Passing %s to Utils::streamFor() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string, resource, StreamInterface, Stringable, Iterator, callable, or null.',
\gettype($resource)
);
if (is_float($resource) && !is_finite($resource)) {
// Normalized only to avoid PHP 8.5's (string) NAN warning
// while deprecated; 3.0 rejects non-finite floats with every
// other non-string scalar.
$resource = is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF');
}
}
$stream = self::tryFopen('php://temp', 'r+');
if ($resource !== '') {
fwrite($stream, (string) $resource);