diff --git a/lib/Client/MessagePart.php b/lib/Client/MessagePart.php index ab1f608..1ea05c1 100644 --- a/lib/Client/MessagePart.php +++ b/lib/Client/MessagePart.php @@ -168,7 +168,7 @@ final class MessagePart $data = [ 'partId' => $this->partId, 'blobId' => $this->partId, - 'cId' => $this->contentId, + 'cid' => $this->contentId, 'type' => $this->mimeType, 'charset' => $this->parameters['charset'] ?? null, 'name' => $this->parameters['name'] ?? $this->dispositionParameters['filename'] ?? null, diff --git a/lib/Providers/MessageAttachment.php b/lib/Providers/MessageAttachment.php index f504973..7db5050 100644 --- a/lib/Providers/MessageAttachment.php +++ b/lib/Providers/MessageAttachment.php @@ -121,10 +121,20 @@ class MessageAttachment implements MessagePartInterface { public function getBlobId(): ?string { return $this->_meta->getBlobId(); } public function getId(): ?string { return $this->_meta->getId(); } + public function getSize(): ?int { return $this->_meta->getSize(); } public function getDisposition(): ?string { return $this->_meta->getDisposition(); } + public function getContentId(): ?string { return $this->_meta->getContentId(); } public function getCharset(): ?string { return $this->_meta->getCharset(); } public function getLanguage(): ?string { return $this->_meta->getLanguage(); } public function getLocation(): ?string { return $this->_meta->getLocation(); } + public function getContent(): ?string { return $this->_contents; } public function getParts(): array { return $this->_meta->getParts(); } - public function jsonSerialize(): array { return $this->_meta->jsonSerialize(); } + public function jsonSerialize(): array { + $data = $this->_meta->jsonSerialize(); + if ($this->_contents !== null) { + $data['content'] = $this->_contents; + } + + return $data; + } } diff --git a/lib/Providers/MessagePart.php b/lib/Providers/MessagePart.php index 616e6e6..8ba4192 100644 --- a/lib/Providers/MessagePart.php +++ b/lib/Providers/MessagePart.php @@ -17,6 +17,24 @@ use KTXM\ProviderImap\Client\MessagePart as ImapMessagePart; */ class MessagePart extends MessagePartMutableAbstract { + /** + * @param array $data + */ + private function hydrateArray(array $data): static { + $this->data = $data; + + if (isset($this->data['subParts']) && is_array($this->data['subParts'])) { + foreach ($this->data['subParts'] as $entry) { + if (is_array($entry)) { + $this->parts[] = (new self())->hydrateArray($entry); + } + } + unset($this->data['subParts']); + } + + return $this; + } + /** * Convert gricob BodyStructure part to message part object * @@ -24,82 +42,11 @@ class MessagePart extends MessagePartMutableAbstract { * @param string $partId numeric part identifier (e.g. "1", "1.1", "2") */ public function fromImap(ImapMessagePart $part, string $partId = '1'): static { + $data = $part->toArray(); + $data['partId'] = $partId; + $data['blobId'] = $data['blobId'] ?? $partId; - $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; + return $this->hydrateArray($data); } } diff --git a/lib/Providers/MessageProperties.php b/lib/Providers/MessageProperties.php index b784da6..42093b0 100644 --- a/lib/Providers/MessageProperties.php +++ b/lib/Providers/MessageProperties.php @@ -82,36 +82,16 @@ class MessageProperties extends MessagePropertiesMutableAbstract { } if ($message->bodyStructure() !== null) { - $this->data[static::PROPERTY_BODY] = $message->bodyStructure()->toArray(); + $body = $message->bodyStructure()->withInjectedSections($message->bodySections() ?? [])->toArray(); + $this->data[static::PROPERTY_BODY] = $body; $attachments = []; - $this->collectAttachments($message->bodyStructure(), $attachments); + $this->collectAttachments($body, $attachments); if ($attachments !== []) { $this->data[static::PROPERTY_ATTACHMENTS] = $attachments; } } - if ($message->bodyStructure() !== null) { - $this->data[static::PROPERTY_BODY] = $message->bodyStructure()->toArray(); - // Recursively add content from bodyValues to matching parts - if (is_array($message->bodySections())) { - $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']] ?? 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], $message->bodySections()); - } - } - $this->data[static::PROPERTY_FLAGS] = []; foreach ($message->flags() as $flag) { $flag = ltrim($flag, '\\'); @@ -132,9 +112,9 @@ class MessageProperties extends MessagePropertiesMutableAbstract { /** * Recursively collect attachment parts from body structure */ - private function collectAttachments(ClientMessagePart $part, array &$attachments): void + private function collectAttachments(array $part, array &$attachments): void { - $children = $part->parts(); + $children = $part['subParts'] ?? []; if ($children !== []) { foreach ($children as $childPart) { $this->collectAttachments($childPart, $attachments); @@ -142,9 +122,9 @@ class MessageProperties extends MessagePropertiesMutableAbstract { return; } - $mimeType = strtolower($part->mimeType()); - $disposition = strtolower($part->disposition() ?? ''); - $name = $part->parameters()['name'] ?? $part->dispositionParameters()['filename'] ?? null; + $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'; @@ -153,7 +133,7 @@ class MessageProperties extends MessagePropertiesMutableAbstract { return; } - $attachments[] = $part->toArray(); + $attachments[] = $part; } } diff --git a/lib/Providers/Provider.php b/lib/Providers/Provider.php index 27312d7..e57c175 100644 --- a/lib/Providers/Provider.php +++ b/lib/Providers/Provider.php @@ -49,10 +49,10 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface, public function jsonSerialize(): array { return [ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, - self::JSON_PROPERTY_LABEL => self::PROVIDER_LABEL, - self::JSON_PROPERTY_CAPABILITIES => $this->providerAbilities, + self::PROPERTY_TYPE => self::JSON_TYPE, + self::PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, + self::PROPERTY_LABEL => self::PROVIDER_LABEL, + self::PROPERTY_CAPABILITIES => $this->providerAbilities, ]; } diff --git a/lib/Providers/Service.php b/lib/Providers/Service.php index e7cd46d..f8c387b 100644 --- a/lib/Providers/Service.php +++ b/lib/Providers/Service.php @@ -44,9 +44,6 @@ use KTXF\Resource\Identifier\EntityIdentifierInterface; use KTXM\ProviderImap\Providers\EntityResource; use KTXM\ProviderImap\Client\Mailbox; -/** - * IMAP Mail Service - */ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface { private const PROVIDER_IDENTIFIER = 'imap'; @@ -56,7 +53,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC private ?string $serviceIdentifier = null; private ?string $serviceLabel = null; private bool $serviceEnabled = false; - private string $primaryAddress = ''; + private array $primaryAddress = []; private array $secondaryAddresses = []; private ?ServiceLocation $location = null; private ?ServiceIdentityBasic $identity = null; @@ -109,6 +106,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC self::CAPABILITY_ENTITY_DELETE => true, self::CAPABILITY_ENTITY_MOVE => true, self::CAPABILITY_ENTITY_COPY => false, + 'EntityTransmit' => true, ]; private RemoteMailService $remoteService; @@ -169,17 +167,17 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC public function jsonSerialize(): array { return array_filter([ - self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER, - self::JSON_PROPERTY_IDENTIFIER => $this->serviceIdentifier, - self::JSON_PROPERTY_LABEL => $this->serviceLabel, - self::JSON_PROPERTY_ENABLED => $this->serviceEnabled, - self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities, - self::JSON_PROPERTY_PRIMARY_ADDRESS => $this->primaryAddress, - self::JSON_PROPERTY_SECONDARY_ADDRESSES => $this->secondaryAddresses, - self::JSON_PROPERTY_LOCATION => $this->location?->jsonSerialize(), - self::JSON_PROPERTY_IDENTITY => $this->identity?->jsonSerialize(), - self::JSON_PROPERTY_AUXILIARY => $this->auxiliary, + self::PROPERTY_TYPE => self::JSON_TYPE, + self::PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER, + self::PROPERTY_IDENTIFIER => $this->serviceIdentifier, + self::PROPERTY_LABEL => $this->serviceLabel, + self::PROPERTY_ENABLED => $this->serviceEnabled, + self::PROPERTY_CAPABILITIES => $this->serviceAbilities, + self::PROPERTY_PRIMARY_ADDRESS => $this->primaryAddress, + self::PROPERTY_SECONDARY_ADDRESSES => $this->secondaryAddresses, + self::PROPERTY_LOCATION => $this->location?->jsonSerialize(), + self::PROPERTY_IDENTITY => $this->identity?->jsonSerialize(), + self::PROPERTY_AUXILIARY => $this->auxiliary, ], fn($v) => $v !== null); } @@ -189,29 +187,29 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC $data = json_decode($data, true, 512, JSON_THROW_ON_ERROR); } - if (isset($data[self::JSON_PROPERTY_ENABLED])) { - $this->setEnabled($data[self::JSON_PROPERTY_ENABLED]); + if (isset($data[self::PROPERTY_ENABLED])) { + $this->setEnabled($data[self::PROPERTY_ENABLED]); } - if (isset($data[self::JSON_PROPERTY_LABEL])) { - $this->setLabel($data[self::JSON_PROPERTY_LABEL]); + if (isset($data[self::PROPERTY_LABEL])) { + $this->setLabel($data[self::PROPERTY_LABEL]); } - if (isset($data[self::JSON_PROPERTY_LOCATION])) { - $this->setLocation($this->freshLocation(null, $data[self::JSON_PROPERTY_LOCATION])); + if (isset($data[self::PROPERTY_LOCATION])) { + $this->setLocation($this->freshLocation(null, $data[self::PROPERTY_LOCATION])); } - if (isset($data[self::JSON_PROPERTY_IDENTITY])) { - $this->setIdentity($this->freshIdentity(null, $data[self::JSON_PROPERTY_IDENTITY])); + if (isset($data[self::PROPERTY_IDENTITY])) { + $this->setIdentity($this->freshIdentity(null, $data[self::PROPERTY_IDENTITY])); } - if (isset($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]) && is_string($data[self::JSON_PROPERTY_PRIMARY_ADDRESS])) { - $this->setPrimaryAddress(new Address($data[self::JSON_PROPERTY_PRIMARY_ADDRESS])); + if (isset($data[self::PROPERTY_PRIMARY_ADDRESS]) && is_string($data[self::PROPERTY_PRIMARY_ADDRESS])) { + $this->setPrimaryAddress(new Address($data[self::PROPERTY_PRIMARY_ADDRESS])); } - if (isset($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES])) { + if (isset($data[self::PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::PROPERTY_SECONDARY_ADDRESSES])) { $this->setSecondaryAddresses(array_map( fn($addr) => new Address(is_array($addr) ? ($addr['address'] ?? $addr) : $addr), - $data[self::JSON_PROPERTY_SECONDARY_ADDRESSES] + $data[self::PROPERTY_SECONDARY_ADDRESSES] )); } - if (isset($data[self::JSON_PROPERTY_AUXILIARY]) && is_array($data[self::JSON_PROPERTY_AUXILIARY])) { - $this->setAuxiliary($data[self::JSON_PROPERTY_AUXILIARY]); + if (isset($data[self::PROPERTY_AUXILIARY]) && is_array($data[self::PROPERTY_AUXILIARY])) { + $this->setAuxiliary($data[self::PROPERTY_AUXILIARY]); } return $this; @@ -261,23 +259,28 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC public function getPrimaryAddress(): AddressInterface { - return new Address($this->primaryAddress); + return Address::fromArray($this->primaryAddress); } public function setPrimaryAddress(AddressInterface $value): static { - $this->primaryAddress = $value->getAddress(); + $this->primaryAddress = $value->toArray(); return $this; } public function getSecondaryAddresses(): array { - return $this->secondaryAddresses; + return array_map( + fn($addr) => $addr instanceof AddressInterface ? $addr : Address::fromArray(is_array($addr) ? $addr : ['address' => (string) $addr]) + , $this->secondaryAddresses); } public function setSecondaryAddresses(array $addresses): static { - $this->secondaryAddresses = $addresses; + $this->secondaryAddresses = array_map( + fn($addr) => $addr instanceof AddressInterface ? $addr : Address::fromArray(is_array($addr) ? $addr : ['address' => (string) $addr]), + $addresses + ); return $this; } @@ -285,7 +288,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC { $address = strtolower(trim($address)); - if ($this->primaryAddress && strtolower($this->primaryAddress) === $address) { + if ($this->primaryAddress && strtolower($this->primaryAddress['address'] ?? '') === $address) { return true; } foreach ($this->secondaryAddresses as $secondary) { @@ -580,7 +583,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC foreach ($identifiers as $collection => $entities) { $uids = array_keys($entities); - foreach ($this->remoteService->entityFetch((string) $collection, ...$uids) as $uid => $message) { + foreach ($this->remoteService->entityFetch((string) $collection, null, ...$uids) as $uid => $message) { $resource = $this->entityFresh(); $resource->fromImap($message, $collection); yield $resource->urn() => $resource; diff --git a/src/components/ImapAuxiliaryPanel.vue b/src/components/ImapAuxiliaryPanel.vue index be0b76e..555f5a7 100644 --- a/src/components/ImapAuxiliaryPanel.vue +++ b/src/components/ImapAuxiliaryPanel.vue @@ -1,6 +1,7 @@ @@ -165,23 +179,66 @@ function sameAddresses(service: ServiceObject, nextPrimaryAddress: string, nextS Configure the primary mailbox identity and any additional sender aliases exposed by this service.

- +
Primary Address
+
+ + +
- +
Secondary Addresses
+
+ + + + mdi-delete-outline + Remove Alias + +
+ + + Add Alias +