* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderJmapc\Providers\Mail; use KTXF\Mail\Object\MessagePropertiesMutableAbstract; /** * Mail Message Properties Implementation */ class MessageProperties extends MessagePropertiesMutableAbstract { private function buildJmapBodyStructure(array &$bodyValues): ?array { $counter = 0; $body = isset($this->data[static::PROPERTY_BODY]) && is_array($this->data[static::PROPERTY_BODY]) ? $this->serializeBodyPartForJmap($this->data[static::PROPERTY_BODY], $bodyValues, $counter) : null; $attachments = []; if (isset($this->data[static::PROPERTY_ATTACHMENTS]) && is_array($this->data[static::PROPERTY_ATTACHMENTS])) { foreach ($this->data[static::PROPERTY_ATTACHMENTS] as $attachment) { if (!is_array($attachment)) { continue; } $serialized = $this->serializeBodyPartForJmap($attachment, $bodyValues, $counter); if ($serialized !== null) { $attachments[] = $serialized; } } } if ($body === null) { if ($attachments === []) { return null; } if (count($attachments) === 1) { return $attachments[0]; } return [ 'type' => 'multipart/mixed', 'subParts' => $attachments, ]; } if ($attachments === []) { return $body; } return [ 'type' => 'multipart/mixed', 'subParts' => array_merge([$body], $attachments), ]; } private function serializeBodyPartForJmap(array $part, array &$bodyValues, int &$counter): ?array { $type = isset($part['type']) && is_string($part['type']) && $part['type'] !== '' ? strtolower($part['type']) : null; if ($type === null) { return null; } $isMultipart = str_starts_with($type, 'multipart/'); $serialized = [ 'type' => $type, ]; if (isset($part['name']) && is_string($part['name']) && $part['name'] !== '') { $serialized['name'] = $part['name']; } if (isset($part['disposition']) && is_string($part['disposition']) && $part['disposition'] !== '') { $serialized['disposition'] = $part['disposition']; } if (isset($part['cid']) && is_string($part['cid']) && $part['cid'] !== '') { $serialized['cid'] = $part['cid']; } if (isset($part['language']) && is_array($part['language']) && $part['language'] !== []) { $serialized['language'] = $part['language']; } if (isset($part['location']) && is_string($part['location']) && $part['location'] !== '') { $serialized['location'] = $part['location']; } if ($isMultipart) { $subParts = []; foreach ($part['subParts'] ?? [] as $subPart) { if (!is_array($subPart)) { continue; } $serializedSubPart = $this->serializeBodyPartForJmap($subPart, $bodyValues, $counter); if ($serializedSubPart !== null) { $subParts[] = $serializedSubPart; } } if ($subParts !== []) { $serialized['subParts'] = $subParts; } return $serialized; } $blobId = isset($part['blobId']) && is_string($part['blobId']) && $part['blobId'] !== '' ? $part['blobId'] : null; $content = isset($part['content']) && is_string($part['content']) ? $part['content'] : null; if ($blobId !== null) { $serialized['blobId'] = $blobId; if (isset($part['charset']) && is_string($part['charset']) && $part['charset'] !== '' && str_starts_with($type, 'text/')) { $serialized['charset'] = $part['charset']; } if (isset($part['size']) && is_int($part['size'])) { $serialized['size'] = $part['size']; } return $serialized; } if ($content !== null && str_starts_with($type, 'text/')) { $partId = isset($part['partId']) && is_string($part['partId']) && $part['partId'] !== '' ? $part['partId'] : 'part-' . (++$counter); $serialized['partId'] = $partId; $bodyValues[$partId] = [ 'value' => $content, 'isEncodingProblem' => false, 'isTruncated' => false, ]; return $serialized; } return $serialized; } /** * Convert JMAP parameters array to mail message properties object * * @param array $parameters JMAP parameters array */ public function fromJmap(array $parameters): static { if (isset($parameters['size'])) { $this->data[static::PROPERTY_SIZE] = $parameters['size']; } if (isset($parameters['headers']) && is_array($parameters['headers'])) { $this->data[static::PROPERTY_HEADERS] = $parameters['headers']; } if (isset($parameters['messageId'])) { $this->data[static::PROPERTY_URID] = $parameters['messageId'][0]; } if (isset($parameters['receivedAt'])) { $this->data[static::PROPERTY_RECEIVED] = $parameters['receivedAt']; } if (isset($parameters['sentAt'])) { $this->data[static::PROPERTY_SENT] = $parameters['sentAt']; } if (isset($parameters['inReplyTo'])) { $this->data[static::PROPERTY_IN_REPLY_TO] = $parameters['inReplyTo']; } if (isset($parameters['references'])) { $this->data[static::PROPERTY_REFERENCES] = is_array($parameters['references']) ? $parameters['references'] : []; } if (isset($parameters['sender'])) { $this->data[static::PROPERTY_SENDER] = $parameters['sender']; } if (isset($parameters['from']) && is_array($parameters['from']) && !empty($parameters['from'])) { $this->data[static::PROPERTY_FROM] = [ 'address' => $parameters['from'][0]['email'] ?? '', 'label' => $parameters['from'][0]['name'] ?? null ]; } if (isset($parameters['replyTo']) && is_array($parameters['replyTo'])) { $this->data[static::PROPERTY_REPLY_TO] = []; foreach ($parameters['replyTo'] as $addr) { $this->data[static::PROPERTY_REPLY_TO][] = [ 'address' => $addr['email'] ?? '', 'label' => $addr['name'] ?? null ]; } } if (isset($parameters['to']) && is_array($parameters['to'])) { $this->data[static::PROPERTY_TO] = []; foreach ($parameters['to'] as $addr) { $this->data[static::PROPERTY_TO][] = [ 'address' => $addr['email'] ?? '', 'label' => $addr['name'] ?? null ]; } } if (isset($parameters['cc']) && is_array($parameters['cc'])) { $this->data[static::PROPERTY_CC] = []; foreach ($parameters['cc'] as $addr) { $this->data[static::PROPERTY_CC][] = [ 'address' => $addr['email'] ?? '', 'label' => $addr['name'] ?? null ]; } } if (isset($parameters['bcc']) && is_array($parameters['bcc'])) { $this->data[static::PROPERTY_BCC] = []; foreach ($parameters['bcc'] as $addr) { $this->data[static::PROPERTY_BCC][] = [ 'address' => $addr['email'] ?? '', 'label' => $addr['name'] ?? null ]; } } if (isset($parameters['subject'])) { $this->data[static::PROPERTY_SUBJECT] = $parameters['subject']; } if (isset($parameters['bodyStructure'])) { $this->data[static::PROPERTY_BODY] = $parameters['bodyStructure']; // Recursively add content from bodyValues to matching parts if (isset($parameters['bodyValues']) && is_array($parameters['bodyValues'])) { $addContentToParts = function(&$structure, $bodyValues) use (&$addContentToParts) { // If this part has a partId and matching bodyValue, add content if (isset($structure['partId']) && isset($bodyValues[$structure['partId']])) { $structure['content'] = $bodyValues[$structure['partId']]['value'] ?? null; } // Recursively process subParts if (isset($structure['subParts']) && is_array($structure['subParts'])) { foreach ($structure['subParts'] as &$subPart) { $addContentToParts($subPart, $bodyValues); } } }; $addContentToParts($this->data[static::PROPERTY_BODY], $parameters['bodyValues']); } } if (isset($parameters['attachments'])) { $this->data[static::PROPERTY_ATTACHMENTS] = $parameters['attachments']; } if (isset($parameters['keywords']) && is_array($parameters['keywords'])) { $this->data[static::PROPERTY_FLAGS] = []; foreach ($parameters['keywords'] as $keyword => $value) { $flag = match($keyword) { '$seen' => 'seen', '$flagged' => 'flagged', '$answered' => 'answered', '$draft' => 'draft', '$deleted' => 'deleted', default => $keyword }; $this->data[static::PROPERTY_FLAGS][$flag] = $value; } } return $this; } /** * Convert mail message properties object to JMAP parameters array */ public function toJmap(): array { $parameters = []; if (isset($this->data['urid'])) { $parameters['messageId'] = [$this->data['urid']]; } if (isset($this->data['size'])) { $parameters['size'] = $this->data['size']; } if (isset($this->data['received'])) { $parameters['receivedAt'] = $this->data['received']; } if (isset($this->data['sent'])) { $parameters['sentAt'] = $this->data['sent']; } if (isset($this->data['inReplyTo'])) { $parameters['inReplyTo'] = $this->data['inReplyTo']; } if (isset($this->data['references'])) { $parameters['references'] = $this->data['references']; } if (isset($this->data['subject'])) { $parameters['subject'] = $this->data['subject']; } if (isset($this->data['snippet'])) { $parameters['preview'] = $this->data['snippet']; } if (isset($this->data['sender'])) { $parameters['sender'] = $this->data['sender']; } if (isset($this->data['from'])) { $parameters['from'] = [[ 'email' => $this->data['from']['address'] ?? '', 'name' => $this->data['from']['label'] ?? null ]]; } if (isset($this->data['to'])) { $parameters['to'] = []; foreach ($this->data['to'] as $addr) { $parameters['to'][] = [ 'email' => $addr['address'] ?? '', 'name' => $addr['label'] ?? null ]; } } if (isset($this->data['cc'])) { $parameters['cc'] = []; foreach ($this->data['cc'] as $addr) { $parameters['cc'][] = [ 'email' => $addr['address'] ?? '', 'name' => $addr['label'] ?? null ]; } } if (isset($this->data['bcc'])) { $parameters['bcc'] = []; foreach ($this->data['bcc'] as $addr) { $parameters['bcc'][] = [ 'email' => $addr['address'] ?? '', 'name' => $addr['label'] ?? null ]; } } if (isset($this->data['replyTo'])) { $parameters['replyTo'] = []; foreach ($this->data['replyTo'] as $addr) { $parameters['replyTo'][] = [ 'email' => $addr['address'] ?? '', 'name' => $addr['label'] ?? null ]; } } if (isset($this->data['flags'])) { $parameters['keywords'] = []; foreach ($this->data['flags'] as $flag => $value) { $keyword = match($flag) { 'seen' => '$seen', 'flagged' => '$flagged', 'answered' => '$answered', 'draft' => '$draft', 'deleted' => '$deleted', default => $flag }; $parameters['keywords'][$keyword] = $value; } } $bodyValues = []; $bodyStructure = $this->buildJmapBodyStructure($bodyValues); if ($bodyStructure !== null) { $parameters['bodyStructure'] = $bodyStructure; $parameters['bodyValues'] = $bodyValues; } if (isset($this->data['headers'])) { $parameters['headers'] = $this->data['headers']; } return $parameters; } }