Files
2026-05-26 08:14:29 +02:00

272 lines
6.6 KiB
PHP

<?php
namespace DirectoryTree\ImapEngine;
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
class BodyStructurePart implements Arrayable, JsonSerializable
{
/**
* Constructor.
*/
public function __construct(
protected string $partNumber,
protected string $type,
protected string $subtype,
protected array $parameters = [],
protected ?string $id = null,
protected ?string $description = null,
protected ?string $encoding = null,
protected ?int $size = null,
protected ?int $lines = null,
protected ?ContentDisposition $disposition = null,
) {}
/**
* Parse a single part BODYSTRUCTURE ListData into a BodyStructurePart.
*/
public static function fromListData(ListData $data, string $partNumber = '1'): static
{
return static::parse($data->tokens(), $partNumber);
}
/**
* Parse a single (non-multipart) part.
*
* @param array<Token|ListData> $tokens
*/
protected static function parse(array $tokens, string $partNumber): static
{
return new static(
partNumber: $partNumber,
type: strtolower(static::tokenValueAt($tokens, 0) ?? 'text'),
subtype: strtolower(static::tokenValueAt($tokens, 1) ?? 'plain'),
parameters: isset($tokens[2]) && $tokens[2] instanceof ListData ? $tokens[2]->toKeyValuePairs() : [],
id: static::tokenValueAt($tokens, 3),
description: static::tokenValueAt($tokens, 4),
encoding: static::tokenValueAt($tokens, 5),
size: static::tokenIntValueAt($tokens, 6),
lines: static::tokenIntValueAt($tokens, 7),
disposition: ContentDisposition::parse($tokens),
);
}
/**
* Safely read a scalar token value from the parsed body structure.
*
* @param array<Token|ListData> $tokens
*/
protected static function tokenValueAt(array $tokens, int $index): ?string
{
$token = $tokens[$index] ?? null;
if (! $token instanceof Token || $token instanceof Nil) {
return null;
}
return $token->value;
}
/**
* Safely read an integer token value from the parsed body structure.
*
* @param array<Token|ListData> $tokens
*/
protected static function tokenIntValueAt(array $tokens, int $index): ?int
{
$value = static::tokenValueAt($tokens, $index);
return $value === null ? null : (int) $value;
}
/**
* Get the part number (e.g., "1", "1.2", "2.1.3").
*/
public function partNumber(): string
{
return $this->partNumber;
}
/**
* Get the MIME type (e.g., "text", "image", "multipart").
*/
public function type(): string
{
return $this->type;
}
/**
* Get the MIME subtype (e.g., "plain", "html", "jpeg", "mixed").
*/
public function subtype(): string
{
return $this->subtype;
}
/**
* Get the full content type (e.g., "text/plain", "multipart/alternative").
*/
public function contentType(): string
{
return "{$this->type}/{$this->subtype}";
}
/**
* Get the parameters (e.g., charset, boundary).
*/
public function parameters(): array
{
return $this->parameters;
}
/**
* Get a specific parameter value.
*/
public function parameter(string $name): ?string
{
return $this->parameters[strtolower($name)] ?? null;
}
/**
* Get the content ID.
*/
public function id(): ?string
{
return $this->id;
}
/**
* Get the content description.
*/
public function description(): ?string
{
return $this->description;
}
/**
* Get the content transfer encoding.
*/
public function encoding(): ?string
{
return $this->encoding;
}
/**
* Get the size in bytes.
*/
public function size(): ?int
{
return $this->size;
}
/**
* Get the number of lines (for text parts).
*/
public function lines(): ?int
{
return $this->lines;
}
/**
* Get the content disposition.
*/
public function disposition(): ?ContentDisposition
{
return $this->disposition;
}
/**
* Get the filename from disposition parameters.
*/
public function filename(): ?string
{
return $this->disposition?->filename() ?? $this->parameters['name'] ?? null;
}
/**
* Get the charset from parameters.
*/
public function charset(): ?string
{
return $this->parameters['charset'] ?? null;
}
/**
* Determine if this is a text part.
*/
public function isText(): bool
{
return $this->type === 'text' && $this->subtype === 'plain';
}
/**
* Determine if this is an HTML part.
*/
public function isHtml(): bool
{
return $this->type === 'text' && $this->subtype === 'html';
}
/**
* Determine if this is an attachment.
*/
public function isAttachment(): bool
{
if ($this->disposition?->isAttachment()) {
return true;
}
// Inline parts are not attachments.
if ($this->disposition?->isInline()) {
return false;
}
// Consider non-text/html parts with filenames as attachments.
if ($this->filename() && ! $this->isText() && ! $this->isHtml()) {
return true;
}
return false;
}
/**
* Determine if this is an inline part.
*/
public function isInline(): bool
{
return $this->disposition?->isInline() ?? false;
}
/**
* Get the array representation.
*/
public function toArray(): array
{
return [
'id' => $this->id,
'type' => $this->type,
'size' => $this->size,
'lines' => $this->lines,
'subtype' => $this->subtype,
'encoding' => $this->encoding,
'parameters' => $this->parameters,
'part_number' => $this->partNumber,
'description' => $this->description,
'content_type' => $this->contentType(),
'disposition' => $this->disposition?->toArray(),
];
}
/**
* Get the JSON representation.
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}