feat: implement content cache

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-09-29 19:56:30 -04:00
parent d28800a04e
commit 0250b5a847
10 changed files with 511 additions and 8 deletions
@@ -33,7 +33,8 @@ final class FetchMessageParser
$uid = self::toInt($attributes['UID'] ?? null, 'FETCH response is missing UID: ' . $raw);
$envelope = is_array($attributes['ENVELOPE'] ?? null) ? $attributes['ENVELOPE'] : null;
$bodyStructure = isset($attributes['BODYSTRUCTURE']) ? self::parseBodyPart($attributes['BODYSTRUCTURE'], '') : null;
$bodySections = self::parseBodySections($attributes, $bodyStructure);
$truncatedSections = [];
$bodySections = self::parseBodySections($attributes, $bodyStructure, $truncatedSections);
$headers = self::parseFetchedHeaders($attributes);
return new Message(
@@ -56,6 +57,7 @@ final class FetchMessageParser
$bodyStructure,
$bodySections,
self::extractReferences($headers),
$truncatedSections,
);
}
@@ -543,7 +545,7 @@ final class FetchMessageParser
* @param array<string, mixed> $attributes
* @return array<string, string>
*/
private static function parseBodySections(array $attributes, ?MessagePart $bodyStructure = null): array
private static function parseBodySections(array $attributes, ?MessagePart $bodyStructure = null, array &$truncated = []): array
{
$sections = [];
@@ -570,7 +572,7 @@ final class FetchMessageParser
}
if ($bodyStructure->isMultipart()) {
$derivedSections = self::sectionsFromBodyText($sections['TEXT'], $bodyStructure);
$derivedSections = self::sectionsFromBodyText($sections['TEXT'], $bodyStructure, $truncated);
unset($sections['TEXT']);
foreach ($derivedSections as $section => $content) {
@@ -581,6 +583,11 @@ final class FetchMessageParser
}
if (str_starts_with($bodyStructure->mimeType(), 'text/')) {
// a single-part body has no closing boundary; a partial fetch shows as fewer octets than declared
$declaredSize = $bodyStructure->size();
if ($declaredSize !== null && strlen($sections['TEXT']) < $declaredSize) {
$truncated[] = $bodyStructure->partId();
}
$sections[$bodyStructure->partId()] ??= $sections['TEXT'];
unset($sections['TEXT']);
}
@@ -616,7 +623,7 @@ final class FetchMessageParser
/**
* @return array<string, string>
*/
private static function sectionsFromBodyText(string $content, MessagePart $part): array
private static function sectionsFromBodyText(string $content, MessagePart $part, array &$truncatedParts = []): array
{
if ($part->isMultipart()) {
$boundary = $part->parameters()['boundary'] ?? '';
@@ -633,7 +640,7 @@ final class FetchMessageParser
}
$segmentTruncated = $truncated && $index === $lastIndex;
foreach (self::sectionsFromMimeEntity($segments[$index], $childPart, $segmentTruncated) as $section => $childContent) {
foreach (self::sectionsFromMimeEntity($segments[$index], $childPart, $segmentTruncated, $truncatedParts) as $section => $childContent) {
$sections[$section] = $childContent;
}
}
@@ -651,7 +658,7 @@ final class FetchMessageParser
/**
* @return array<string, string>
*/
private static function sectionsFromMimeEntity(string $content, MessagePart $part, bool $truncated = false): array
private static function sectionsFromMimeEntity(string $content, MessagePart $part, bool $truncated = false, array &$truncatedParts = []): array
{
// a truncated entity cut off inside its headers has no usable body
if ($truncated && !str_contains($content, "\r\n\r\n") && !str_contains($content, "\n\n")) {
@@ -661,13 +668,17 @@ final class FetchMessageParser
[, $body] = self::splitMimeEntity($content);
if ($part->isMultipart()) {
return self::sectionsFromBodyText($body, $part);
return self::sectionsFromBodyText($body, $part, $truncatedParts);
}
if (!str_starts_with($part->mimeType(), 'text/')) {
return [];
}
if ($truncated) {
$truncatedParts[] = $part->partId();
}
return [$part->partId() => $body];
}