generated from Nodarx/template
65fdb23fa4
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
269 lines
8.7 KiB
PHP
269 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace KTXM\ProviderImap\Providers;
|
|
|
|
use DateTimeImmutable;
|
|
use DateTimeInterface;
|
|
use KTXM\ProviderImap\Client\Message;
|
|
use KTXM\ProviderImap\Client\MessagePart as ClientMessagePart;
|
|
use KTXF\Mail\Object\MessagePropertiesMutableAbstract;
|
|
|
|
/**
|
|
* Mail Message Properties Implementation
|
|
*/
|
|
class MessageProperties extends MessagePropertiesMutableAbstract {
|
|
|
|
/**
|
|
* Part ids of text sections that were cut off when fetched (cache only, not part of the API shape)
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
private array $incompleteSections = [];
|
|
|
|
/**
|
|
* Convert IMAP data to mail message properties object.
|
|
*/
|
|
public function fromImap(Message $message): static
|
|
{
|
|
$this->data[static::PROPERTY_SIZE] = $message->size();
|
|
$this->incompleteSections = $message->truncatedSections();
|
|
|
|
if ($message->messageId() !== null) {
|
|
$this->data[static::PROPERTY_URID] = $message->messageId();
|
|
}
|
|
|
|
if ($message->inReplyTo() !== null) {
|
|
$this->data[static::PROPERTY_IN_REPLY_TO] = $message->inReplyTo();
|
|
}
|
|
|
|
if ($message->references() !== []) {
|
|
$this->data[static::PROPERTY_REFERENCES] = $message->references();
|
|
}
|
|
|
|
$receivedAt = $message->receivedAt() ?? $message->internalDate();
|
|
if ($receivedAt !== null) {
|
|
$date = new DateTimeImmutable($receivedAt);
|
|
$this->data[static::PROPERTY_RECEIVED] = $date->format(DateTimeInterface::ATOM);
|
|
}
|
|
|
|
if ($message->sentAt() !== null) {
|
|
$date = new DateTimeImmutable($message->sentAt());
|
|
$this->data[static::PROPERTY_SENT] = $date->format(DateTimeInterface::ATOM);
|
|
}
|
|
|
|
if ($message->sender() !== []) {
|
|
$this->data[static::PROPERTY_SENDER] = $message->sender()[0]->toArray();
|
|
}
|
|
|
|
if ($message->from() !== []) {
|
|
$this->data[static::PROPERTY_FROM] = $message->from()[0]->toArray();
|
|
}
|
|
|
|
$addressProperties = [
|
|
'to' => static::PROPERTY_TO,
|
|
'cc' => static::PROPERTY_CC,
|
|
'bcc' => static::PROPERTY_BCC,
|
|
'replyTo' => static::PROPERTY_REPLY_TO,
|
|
];
|
|
|
|
foreach ($addressProperties as $field => $property) {
|
|
$addresses = $message->{$field}();
|
|
if ($addresses === []) {
|
|
continue;
|
|
}
|
|
|
|
$this->data[$property] = array_map(
|
|
static fn ($address): array => $address->toArray(),
|
|
$addresses,
|
|
);
|
|
}
|
|
|
|
if ($message->subject() !== null) {
|
|
$this->data[static::PROPERTY_SUBJECT] = $message->subject();
|
|
}
|
|
|
|
if ($message->bodyStructure() !== null) {
|
|
$body = $message->bodyStructure()->withInjectedSections($message->bodySections() ?? [])->toArray();
|
|
$this->data[static::PROPERTY_BODY] = $body;
|
|
|
|
$attachments = [];
|
|
$this->collectAttachments($body, $attachments);
|
|
if ($attachments !== []) {
|
|
$this->data[static::PROPERTY_ATTACHMENTS] = $attachments;
|
|
}
|
|
}
|
|
|
|
$this->data[static::PROPERTY_FLAGS] = array_fill_keys(self::normalizeFlags($message->flags()), true);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Normalise IMAP flags (e.g. "\\Seen", "$Forwarded") to flag names (e.g. "seen", "$forwarded").
|
|
*
|
|
* @param string[] $flags
|
|
* @return list<string>
|
|
*/
|
|
public static function normalizeFlags(array $flags): array
|
|
{
|
|
$normalized = [];
|
|
foreach ($flags as $flag) {
|
|
$normalized[] = strtolower(ltrim($flag, '\\'));
|
|
}
|
|
|
|
return array_values(array_unique($normalized));
|
|
}
|
|
|
|
// ── Cache (meta store / content store) ───────────────────────────────────
|
|
|
|
/**
|
|
* Part ids of text sections that were cut off when fetched and must be completed on open.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
public function getIncompleteSections(): array
|
|
{
|
|
return $this->incompleteSections;
|
|
}
|
|
|
|
public function setIncompleteSections(string ...$partIds): static
|
|
{
|
|
$this->incompleteSections = array_values(array_unique($partIds));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Serialise the fields the meta store needs for list, filter and sort.
|
|
*
|
|
* Dates are normalised to UTC so they sort correctly as strings; the original
|
|
* values (with their offsets) are kept in the content store.
|
|
*/
|
|
public function toCacheMeta(): array
|
|
{
|
|
return [
|
|
'received' => self::cacheDate($this->data[static::PROPERTY_RECEIVED] ?? null),
|
|
'sent' => self::cacheDate($this->data[static::PROPERTY_SENT] ?? null),
|
|
'size' => $this->data[static::PROPERTY_SIZE] ?? 0,
|
|
'subject' => $this->data[static::PROPERTY_SUBJECT] ?? '',
|
|
'from' => $this->data[static::PROPERTY_FROM] ?? null,
|
|
'to' => $this->data[static::PROPERTY_TO] ?? [],
|
|
'cc' => $this->data[static::PROPERTY_CC] ?? [],
|
|
'urid' => $this->data[static::PROPERTY_URID] ?? null,
|
|
'inReplyTo' => $this->data[static::PROPERTY_IN_REPLY_TO] ?? null,
|
|
'references' => $this->data[static::PROPERTY_REFERENCES] ?? [],
|
|
// list of set flags, not a map: keywords such as $Forwarded are not valid field names
|
|
'flags' => array_keys(array_filter($this->data[static::PROPERTY_FLAGS] ?? [])),
|
|
'hasAttachments' => ($this->data[static::PROPERTY_ATTACHMENTS] ?? []) !== [],
|
|
'preview' => $this->cachePreview(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Restore the mutable fields from a meta store document.
|
|
*
|
|
* Only flags are taken from the meta store; everything else comes from the
|
|
* content store, which holds the original values.
|
|
*/
|
|
public function fromCacheMeta(array $document): static
|
|
{
|
|
$this->data[static::PROPERTY_FLAGS] = array_fill_keys(
|
|
array_map('strval', (array) ($document['flags'] ?? [])),
|
|
true,
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Serialise the immutable message content for the content store.
|
|
*/
|
|
public function toCacheContent(): array
|
|
{
|
|
$content = $this->data;
|
|
unset($content[static::PROPERTY_FLAGS]);
|
|
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Restore the immutable message content from the content store, keeping any flags already set.
|
|
*/
|
|
public function fromCacheContent(array $content): static
|
|
{
|
|
unset($content[static::PROPERTY_FLAGS]);
|
|
$flags = $this->data[static::PROPERTY_FLAGS] ?? null;
|
|
|
|
$this->data = $content;
|
|
if ($flags !== null) {
|
|
$this->data[static::PROPERTY_FLAGS] = $flags;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
private function cachePreview(int $length = 200): string
|
|
{
|
|
$text = $this->getBodyTextPlain();
|
|
if ($text === null) {
|
|
$html = $this->getBodyTextHtml();
|
|
$text = $html !== null
|
|
? html_entity_decode(strip_tags((string) preg_replace('/<(style|script)\b[^>]*>.*?<\/\1>/is', ' ', $html)), ENT_QUOTES | ENT_HTML5, 'UTF-8')
|
|
: '';
|
|
}
|
|
|
|
$text = trim((string) preg_replace('/\s+/u', ' ', $text));
|
|
|
|
return mb_substr($text, 0, $length);
|
|
}
|
|
|
|
private static function cacheDate(?string $value): ?string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return (new DateTimeImmutable($value))
|
|
->setTimezone(new \DateTimeZone('UTC'))
|
|
->format('Y-m-d\TH:i:s\Z');
|
|
} catch (\Exception) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively collect attachment parts from body structure
|
|
*/
|
|
private function collectAttachments(array $part, array &$attachments): void
|
|
{
|
|
$children = $part['subParts'] ?? [];
|
|
if ($children !== []) {
|
|
foreach ($children as $childPart) {
|
|
$this->collectAttachments($childPart, $attachments);
|
|
}
|
|
return;
|
|
}
|
|
|
|
$mimeType = strtolower((string)($part['type'] ?? ''));
|
|
$disposition = strtolower((string)($part['disposition'] ?? ''));
|
|
$name = $part['name'] ?? null;
|
|
$isInlineText = str_starts_with($mimeType, 'text/')
|
|
&& in_array($mimeType, ['text/plain', 'text/html'], true)
|
|
&& $disposition !== 'attachment';
|
|
|
|
if ($isInlineText || ($disposition === '' && $name === null)) {
|
|
return;
|
|
}
|
|
|
|
$attachments[] = $part;
|
|
}
|
|
|
|
}
|