Files
provider_imap/lib/Providers/MessagePart.php
Sebastian Krupinski 6fac63b7d2 feat: speed improvements
Signed-off-by: Sebastian Krupinski <root@LAPTOP-7DVOR6NC>
2026-03-03 22:07:16 -05:00

186 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Providers;
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\MultiPart;
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\Part;
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\SinglePart;
use KTXF\Mail\Object\MessagePartMutableAbstract;
/**
* Mail Message Part Implementation
*/
class MessagePart extends MessagePartMutableAbstract {
/**
* Convert gricob BodyStructure part to message part object
*
* @param Part $part gricob BodyStructure Part (SinglePart or MultiPart)
* @param string $partId numeric part identifier (e.g. "1", "1.1", "2")
*/
public function fromImap(Part $part, string $partId = '1'): static {
$this->data['partId'] = $partId;
if ($part instanceof SinglePart) {
$mimeType = strtolower($part->type) . '/' . strtolower($part->subtype);
$this->data['type'] = $mimeType;
if ($part->id !== null) {
$this->data['blobId'] = trim($part->id, '<>');
}
// Content-Type parameters (name, charset, etc.)
if (!empty($part->attributes)) {
foreach ($part->attributes as $key => $value) {
$keyLower = strtolower($key);
if ($keyLower === 'name') {
$this->data['name'] = $value;
} elseif ($keyLower === 'charset') {
$this->data['charset'] = $value;
}
}
}
if ($part->encoding !== null) {
$this->data['encoding'] = strtolower($part->encoding);
}
if ($part->size !== null) {
$this->data['size'] = $part->size;
}
if ($part->disposition !== null) {
$this->data['disposition'] = strtolower($part->disposition->type);
// disposition filename attribute
if (!empty($part->disposition->attributes)) {
foreach ($part->disposition->attributes as $key => $value) {
if (strtolower($key) === 'filename') {
$this->data['name'] = $this->data['name'] ?? $value;
}
}
}
}
if (!empty($part->language)) {
$this->data['language'] = implode(',', $part->language);
}
if ($part->location !== null) {
$this->data['location'] = $part->location;
}
} elseif ($part instanceof MultiPart) {
$this->data['type'] = 'multipart/' . strtolower($part->subtype);
if ($part->disposition !== null) {
$this->data['disposition'] = strtolower($part->disposition->type);
}
if (!empty($part->language)) {
$this->data['language'] = implode(',', $part->language);
}
if ($part->location !== null) {
$this->data['location'] = $part->location;
}
// Recursively process sub-parts
// When this part has no section ID (root multipart) children are
// numbered "1", "2", … to match IMAP section numbering.
foreach ($part->parts as $index => $subPart) {
$subPartId = ($partId === '') ? (string)($index + 1) : $partId . '.' . ($index + 1);
$this->parts[] = (new MessagePart())->fromImap($subPart, $subPartId);
}
}
return $this;
}
/**
* Convert message part to store array
*/
public function toStore(): array {
$data = $this->data;
if (count($this->parts) > 0) {
$data['subParts'] = [];
foreach ($this->parts as $subPart) {
if ($subPart instanceof MessagePart) {
$data['subParts'][] = $subPart->toStore();
}
}
} else {
$data['subParts'] = null;
}
return $data;
}
/**
* Hydrate message part from store array
*/
public function fromStore(array $data): static {
if (isset($data['subParts']) && is_array($data['subParts'])) {
foreach ($data['subParts'] as $subPart) {
$this->parts[] = (new MessagePart())->fromStore($subPart);
}
unset($data['subParts']);
}
$this->data = $data;
return $this;
}
/**
* Inject decoded body content from a map of IMAP section-ID → raw encoded text.
*
* Walks the MessagePart tree recursively. For each text/* leaf part whose
* partId is present in $sectionMap the raw text is decoded according to the
* part's Content-Transfer-Encoding and converted to UTF-8 before being
* stored in 'content'. Binary parts (images, PDFs, …) are skipped.
*
* @param array<string,string> $sectionMap Keys: IMAP section IDs (e.g. "1", "1.2");
* Values: raw (transfer-encoded) body text
*/
public function injectSections(array $sectionMap): void
{
// MultiPart: recurse into children
if (!empty($this->parts)) {
foreach ($this->parts as $childPart) {
if ($childPart instanceof MessagePart) {
$childPart->injectSections($sectionMap);
}
}
return;
}
// SinglePart: only inject decoded content for text/* MIME types
$type = strtolower($this->data['type'] ?? '');
if (!str_starts_with($type, 'text/')) {
return;
}
$partId = $this->data['partId'] ?? null;
if ($partId === null || !array_key_exists($partId, $sectionMap)) {
return;
}
$raw = $sectionMap[$partId];
$encoding = strtolower($this->data['encoding'] ?? '7bit');
$decoded = match ($encoding) {
'quoted-printable' => quoted_printable_decode($raw),
'base64' => base64_decode($raw, strict: false),
default => $raw, // 7bit, 8bit, binary
};
$charset = $this->data['charset'] ?? 'us-ascii';
$this->data['content'] = MessageProperties::toUtf8($decoded, $charset);
}
}