From 869f2e4fb4e3e6bf4004645aaa6de28349ad5b0b Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Tue, 16 Jun 2026 13:12:30 -0400 Subject: [PATCH] refactor: message object properties Signed-off-by: Sebastian Krupinski --- shared/lib/Mail/Object/Address.php | 33 +-- shared/lib/Mail/Object/AddressInterface.php | 11 +- shared/lib/Mail/Object/Attachment.php | 194 ------------------ .../lib/Mail/Object/AttachmentInterface.php | 93 --------- shared/lib/Mail/Object/MessagePart.php | 37 ++++ .../Mail/Object/MessagePartBaseAbstract.php | 43 +++- .../lib/Mail/Object/MessagePartInterface.php | 40 ++++ .../Object/MessagePartMutableAbstract.php | 30 +++ .../Object/MessagePropertiesBaseAbstract.php | 134 ++++++++++-- .../Object/MessagePropertiesBaseInterface.php | 4 +- .../MessagePropertiesMutableAbstract.php | 131 +++++++++--- .../MessagePropertiesMutableInterface.php | 8 +- .../Service/ServiceEntitySubmitInterface.php | 54 +++++ .../ServiceEntityTransmitInterface.php | 48 ----- .../Mail/Submission/EntitySubmitResult.php | 52 +++++ 15 files changed, 506 insertions(+), 406 deletions(-) delete mode 100644 shared/lib/Mail/Object/Attachment.php delete mode 100644 shared/lib/Mail/Object/AttachmentInterface.php create mode 100644 shared/lib/Mail/Object/MessagePart.php create mode 100644 shared/lib/Mail/Service/ServiceEntitySubmitInterface.php delete mode 100644 shared/lib/Mail/Service/ServiceEntityTransmitInterface.php create mode 100644 shared/lib/Mail/Submission/EntitySubmitResult.php diff --git a/shared/lib/Mail/Object/Address.php b/shared/lib/Mail/Object/Address.php index f98faaa..7347ee2 100644 --- a/shared/lib/Mail/Object/Address.php +++ b/shared/lib/Mail/Object/Address.php @@ -25,16 +25,6 @@ class Address implements AddressInterface { private ?string $name = null, ) {} - /** - * @inheritDoc - */ - public function jsonSerialize(): array { - return array_filter([ - self::JSON_PROPERTY_ADDRESS => $this->address, - self::JSON_PROPERTY_LABEL => $this->name, - ], fn($v) => $v !== null && $v !== ''); - } - /** * Creates an Address from a formatted string * @@ -66,17 +56,34 @@ class Address implements AddressInterface { * * @since 2025.05.01 * - * @param array $data Array with 'address' and optional 'name' keys + * @param array $data * * @return self */ public static function fromArray(array $data): self { return new self( - $data[self::JSON_PROPERTY_ADDRESS] ?? $data['address'] ?? '', - $data[self::JSON_PROPERTY_LABEL] ?? $data['name'] ?? null, + $data[self::PROPERTY_ADDRESS] ?? $data['address'] ?? '', + $data[self::PROPERTY_LABEL] ?? $data['label'] ?? null, ); } + /** + * @inheritDoc + */ + public function jsonSerialize(): array { + return array_filter([ + self::PROPERTY_ADDRESS => $this->address, + self::PROPERTY_LABEL => $this->name, + ], fn($v) => $v !== null && $v !== ''); + } + + /** + * @inheritDoc + */ + public function toArray(): array { + return $this->jsonSerialize(); + } + /** * @inheritDoc */ diff --git a/shared/lib/Mail/Object/AddressInterface.php b/shared/lib/Mail/Object/AddressInterface.php index fa67592..1f1a8dd 100644 --- a/shared/lib/Mail/Object/AddressInterface.php +++ b/shared/lib/Mail/Object/AddressInterface.php @@ -20,8 +20,15 @@ use KTXF\Json\JsonSerializable; */ interface AddressInterface extends JsonSerializable { - public const JSON_PROPERTY_ADDRESS = 'address'; - public const JSON_PROPERTY_LABEL = 'label'; + public const PROPERTY_ADDRESS = 'address'; + public const PROPERTY_LABEL = 'label'; + + /* + * Converts the Address to an array representation + * + * @return array + */ + public function toArray(): array; /** * Gets the email address diff --git a/shared/lib/Mail/Object/Attachment.php b/shared/lib/Mail/Object/Attachment.php deleted file mode 100644 index 58edccf..0000000 --- a/shared/lib/Mail/Object/Attachment.php +++ /dev/null @@ -1,194 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXF\Mail\Object; - -/** - * Attachment Implementation - * - * @since 2025.05.01 - */ -class Attachment implements AttachmentInterface { - - /** - * @param string $name File name - * @param string $mimeType MIME type - * @param string $content Binary content - * @param string|null $id Attachment ID - * @param int|null $size Size in bytes - * @param string|null $contentId Content-ID for inline attachments - * @param bool $inline Whether inline attachment - */ - public function __construct( - private string $name, - private string $mimeType, - private string $content, - private ?string $id = null, - private ?int $size = null, - private ?string $contentId = null, - private bool $inline = false, - ) { - if ($this->size === null) { - $this->size = strlen($this->content); - } - } - - /** - * @inheritDoc - */ - public function jsonSerialize(): array { - return array_filter([ - self::JSON_PROPERTY_ID => $this->id, - self::JSON_PROPERTY_NAME => $this->name, - self::JSON_PROPERTY_MIME_TYPE => $this->mimeType, - self::JSON_PROPERTY_SIZE => $this->size, - self::JSON_PROPERTY_CONTENT_ID => $this->contentId, - self::JSON_PROPERTY_INLINE => $this->inline ?: null, - 'contentBase64' => $this->getContentBase64(), - ], fn($v) => $v !== null); - } - - - /** - * Creates an attachment from a file path - * - * @since 2025.05.01 - * - * @param string $path File path - * @param string|null $name Override file name - * @param string|null $mimeType Override MIME type - * - * @return self - */ - public static function fromFile(string $path, ?string $name = null, ?string $mimeType = null): self { - $content = file_get_contents($path); - $name = $name ?? basename($path); - $mimeType = $mimeType ?? mime_content_type($path) ?: 'application/octet-stream'; - - return new self($name, $mimeType, $content); - } - - /** - * Creates an attachment from base64 encoded content - * - * @since 2025.05.01 - * - * @param string $name File name - * @param string $mimeType MIME type - * @param string $base64Content Base64 encoded content - * - * @return self - */ - public static function fromBase64(string $name, string $mimeType, string $base64Content): self { - return new self($name, $mimeType, base64_decode($base64Content)); - } - - /** - * Creates an inline attachment for embedding in HTML - * - * @since 2025.05.01 - * - * @param string $name File name - * @param string $mimeType MIME type - * @param string $content Binary content - * @param string $contentId Content-ID (without cid: prefix) - * - * @return self - */ - public static function inline(string $name, string $mimeType, string $content, string $contentId): self { - return new self($name, $mimeType, $content, null, null, $contentId, true); - } - - /** - * Creates from array data - * - * @since 2025.05.01 - * - * @param array $data - * - * @return self - */ - public static function fromArray(array $data): self { - $content = $data['content'] ?? ''; - if (isset($data['contentBase64'])) { - $content = base64_decode($data['contentBase64']); - } - - return new self( - $data[self::JSON_PROPERTY_NAME] ?? $data['name'] ?? '', - $data[self::JSON_PROPERTY_MIME_TYPE] ?? $data['mimeType'] ?? 'application/octet-stream', - $content, - $data[self::JSON_PROPERTY_ID] ?? $data['id'] ?? null, - $data[self::JSON_PROPERTY_SIZE] ?? $data['size'] ?? null, - $data[self::JSON_PROPERTY_CONTENT_ID] ?? $data['contentId'] ?? null, - $data[self::JSON_PROPERTY_INLINE] ?? $data['inline'] ?? false, - ); - } - - /** - * @inheritDoc - */ - public function getId(): ?string { - return $this->id; - } - - /** - * @inheritDoc - */ - public function getName(): string { - return $this->name; - } - - /** - * @inheritDoc - */ - public function getMimeType(): string { - return $this->mimeType; - } - - /** - * @inheritDoc - */ - public function getSize(): ?int { - return $this->size; - } - - /** - * @inheritDoc - */ - public function getContentId(): ?string { - return $this->contentId; - } - - /** - * @inheritDoc - */ - public function isInline(): bool { - return $this->inline; - } - - /** - * @inheritDoc - */ - public function getContent(): string { - return $this->content; - } - - /** - * Gets the content as base64 encoded string - * - * @since 2025.05.01 - * - * @return string - */ - public function getContentBase64(): string { - return base64_encode($this->content); - } - -} diff --git a/shared/lib/Mail/Object/AttachmentInterface.php b/shared/lib/Mail/Object/AttachmentInterface.php deleted file mode 100644 index ef1d57f..0000000 --- a/shared/lib/Mail/Object/AttachmentInterface.php +++ /dev/null @@ -1,93 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXF\Mail\Object; - -use KTXF\Json\JsonSerializable; - -/** - * Attachment Interface - * - * Represents a file attachment on a mail message. - * - * @since 2025.05.01 - */ -interface AttachmentInterface extends JsonSerializable { - - public const JSON_PROPERTY_ID = 'id'; - public const JSON_PROPERTY_NAME = 'name'; - public const JSON_PROPERTY_MIME_TYPE = 'mimeType'; - public const JSON_PROPERTY_SIZE = 'size'; - public const JSON_PROPERTY_CONTENT_ID = 'contentId'; - public const JSON_PROPERTY_INLINE = 'inline'; - - /** - * Gets the attachment identifier - * - * @since 2025.05.01 - * - * @return string|null Attachment ID or null for new attachments - */ - public function getId(): ?string; - - /** - * Gets the file name - * - * @since 2025.05.01 - * - * @return string File name (e.g., "document.pdf") - */ - public function getName(): string; - - /** - * Gets the MIME type - * - * @since 2025.05.01 - * - * @return string MIME type (e.g., "application/pdf") - */ - public function getMimeType(): string; - - /** - * Gets the file size in bytes - * - * @since 2025.05.01 - * - * @return int|null Size in bytes or null if unknown - */ - public function getSize(): ?int; - - /** - * Gets the Content-ID for inline attachments - * - * @since 2025.05.01 - * - * @return string|null Content-ID for referencing in HTML body (e.g., "cid:image1") - */ - public function getContentId(): ?string; - - /** - * Checks if this is an inline attachment (embedded in body) - * - * @since 2025.05.01 - * - * @return bool True if inline, false if regular attachment - */ - public function isInline(): bool; - - /** - * Gets the attachment content - * - * @since 2025.05.01 - * - * @return string Binary content of the attachment - */ - public function getContent(): string; - -} diff --git a/shared/lib/Mail/Object/MessagePart.php b/shared/lib/Mail/Object/MessagePart.php new file mode 100644 index 0000000..f887ad5 --- /dev/null +++ b/shared/lib/Mail/Object/MessagePart.php @@ -0,0 +1,37 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Mail\Object; + +/** + * Shared Message Part implementation matching the frontend JSON shape. + */ +class MessagePart extends MessagePartMutableAbstract { + + public static function fromArray(array $data): self { + $part = new self(); + $part->data = $data; + + if (isset($part->data[MessagePartInterface::PROPERTY_SUB_PARTS]) && is_array($part->data[MessagePartInterface::PROPERTY_SUB_PARTS])) { + foreach ($part->data[MessagePartInterface::PROPERTY_SUB_PARTS] as $entry) { + if (is_object($entry)) { + $entry = get_object_vars($entry); + } + + if (is_array($entry)) { + $part->parts[] = self::fromArray($entry); + } + } + + unset($part->data[MessagePartInterface::PROPERTY_SUB_PARTS]); + } + + return $part; + } +} \ No newline at end of file diff --git a/shared/lib/Mail/Object/MessagePartBaseAbstract.php b/shared/lib/Mail/Object/MessagePartBaseAbstract.php index 0abef34..8a2cf4a 100644 --- a/shared/lib/Mail/Object/MessagePartBaseAbstract.php +++ b/shared/lib/Mail/Object/MessagePartBaseAbstract.php @@ -32,9 +32,9 @@ abstract class MessagePartBaseAbstract implements MessagePartInterface { /** * Constructor * - * @param array &$data Reference to data array + * @param array|null &$data Reference to data array */ - public function __construct(array &$data = null) { + public function __construct(array|null &$data = null) { if ($data === null) { $data = []; } @@ -52,49 +52,70 @@ abstract class MessagePartBaseAbstract implements MessagePartInterface { * @inheritDoc */ public function getId(): ?string { - return $this->data['partId'] ?? null; + return $this->data[MessagePartInterface::PROPERTY_PART_ID] ?? null; + } + + /** + * @inheritDoc + */ + public function getSize(): ?int { + return $this->data[MessagePartInterface::PROPERTY_SIZE] ?? null; } /** * @inheritDoc */ public function getType(): ?string { - return $this->data['type'] ?? null; + return $this->data[MessagePartInterface::PROPERTY_TYPE] ?? null; } /** * @inheritDoc */ public function getDisposition(): ?string { - return $this->data['disposition'] ?? null; + return $this->data[MessagePartInterface::PROPERTY_DISPOSITION] ?? null; } /** * @inheritDoc */ public function getName(): ?string { - return $this->data['name'] ?? null; + return $this->data[MessagePartInterface::PROPERTY_NAME] ?? null; } /** * @inheritDoc */ public function getCharset(): ?string { - return $this->data['charset'] ?? null; + return $this->data[MessagePartInterface::PROPERTY_CHARSET] ?? null; + } + + /** + * @inheritDoc + */ + public function getContentId(): ?string { + return $this->data[MessagePartInterface::PROPERTY_CONTENT_ID] ?? null; } /** * @inheritDoc */ public function getLanguage(): ?string { - return $this->data['language'] ?? null; + return $this->data[MessagePartInterface::PROPERTY_LANGUAGE] ?? null; } /** * @inheritDoc */ public function getLocation(): ?string { - return $this->data['location'] ?? null; + return $this->data[MessagePartInterface::PROPERTY_LOCATION] ?? null; + } + + /** + * @inheritDoc + */ + public function getContent(): ?string { + return $this->data[MessagePartInterface::PROPERTY_CONTENT] ?? null; } /** @@ -111,9 +132,9 @@ abstract class MessagePartBaseAbstract implements MessagePartInterface { $result = $this->data; if (!empty($this->parts)) { - $result['subParts'] = []; + $result[MessagePartInterface::PROPERTY_SUB_PARTS] = []; foreach ($this->parts as $part) { - $result['subParts'][] = $part->jsonSerialize(); + $result[MessagePartInterface::PROPERTY_SUB_PARTS][] = $part->jsonSerialize(); } } diff --git a/shared/lib/Mail/Object/MessagePartInterface.php b/shared/lib/Mail/Object/MessagePartInterface.php index 4a576cb..c2c0554 100644 --- a/shared/lib/Mail/Object/MessagePartInterface.php +++ b/shared/lib/Mail/Object/MessagePartInterface.php @@ -20,6 +20,19 @@ use KTXF\Json\JsonSerializable; */ interface MessagePartInterface extends JsonSerializable { + public const PROPERTY_PART_ID = 'partId'; + public const PROPERTY_BLOB_ID = 'blobId'; + public const PROPERTY_BLOB_IDSIZE = 'size'; + public const PROPERTY_NAME = 'name'; + public const PROPERTY_TYPE = 'type'; + public const PROPERTY_CHARSET = 'charset'; + public const PROPERTY_DISPOSITION = 'disposition'; + public const PROPERTY_CONTENT_ID = 'cid'; + public const PROPERTY_LANGUAGE = 'language'; + public const PROPERTY_LOCATION = 'location'; + public const PROPERTY_CONTENT = 'content'; + public const PROPERTY_SUB_PARTS = 'subParts'; + /** * Gets the blob identifier * @@ -38,6 +51,15 @@ interface MessagePartInterface extends JsonSerializable { */ public function getId(): ?string; + /** + * Gets the part size in bytes + * + * @since 2026.06.15 + * + * @return int|null + */ + public function getSize(): ?int; + /** * Gets the MIME type * @@ -56,6 +78,15 @@ interface MessagePartInterface extends JsonSerializable { */ public function getDisposition(): ?string; + /** + * Gets the Content-ID for inline parts + * + * @since 2026.06.15 + * + * @return string|null + */ + public function getContentId(): ?string; + /** * Gets the part name * @@ -92,6 +123,15 @@ interface MessagePartInterface extends JsonSerializable { */ public function getLocation(): ?string; + /** + * Gets the decoded part content + * + * @since 2026.06.15 + * + * @return string|null + */ + public function getContent(): ?string; + /** * Gets the sub-parts * diff --git a/shared/lib/Mail/Object/MessagePartMutableAbstract.php b/shared/lib/Mail/Object/MessagePartMutableAbstract.php index 92f0456..07c7146 100644 --- a/shared/lib/Mail/Object/MessagePartMutableAbstract.php +++ b/shared/lib/Mail/Object/MessagePartMutableAbstract.php @@ -46,6 +46,16 @@ abstract class MessagePartMutableAbstract extends MessagePartBaseAbstract { return $this; } + /** + * Sets the part size + * + * @since 2026.06.15 + */ + public function setSize(?int $value): static { + $this->data['size'] = $value; + return $this; + } + /** * Sets the MIME type * @@ -102,6 +112,16 @@ abstract class MessagePartMutableAbstract extends MessagePartBaseAbstract { return $this; } + /** + * Sets the content id + * + * @since 2026.06.15 + */ + public function setContentId(?string $value): static { + $this->data['cid'] = $value; + return $this; + } + /** * Sets the language * @@ -130,6 +150,16 @@ abstract class MessagePartMutableAbstract extends MessagePartBaseAbstract { return $this; } + /** + * Sets the content payload + * + * @since 2026.06.15 + */ + public function setContent(?string $value): static { + $this->data['content'] = $value; + return $this; + } + /** * Sets the sub-parts * diff --git a/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php b/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php index fac63d1..f975f59 100644 --- a/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php +++ b/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace KTXF\Mail\Object; use DateTimeImmutable; +use Exception; use KTXF\Resource\Provider\Node\NodePropertiesBaseAbstract; /** @@ -23,6 +24,109 @@ abstract class MessagePropertiesBaseAbstract extends NodePropertiesBaseAbstract protected string $type = 'mail.message'; + protected function extractBodyContent(?MessagePartInterface $part, string $type): ?string { + if ($part === null) { + return null; + } + + if ($part->getType() === $type) { + $content = $part->getContent(); + return is_string($content) && $content !== '' ? $content : null; + } + + foreach ($part->getParts() as $subPart) { + $content = $this->extractBodyContent($subPart, $type); + if ($content !== null) { + return $content; + } + } + + return null; + } + + protected function materializeAddress(mixed $value): ?AddressInterface { + if ($value instanceof AddressInterface) { + return $value; + } + + if (is_string($value) && $value !== '') { + return Address::fromString($value); + } + + if (!is_array($value) || $value === []) { + return null; + } + + if (array_is_list($value)) { + return $this->materializeAddress($value[0] ?? null); + } + + return Address::fromArray([ + AddressInterface::PROPERTY_ADDRESS => $value[AddressInterface::PROPERTY_ADDRESS], + AddressInterface::PROPERTY_LABEL => $value[AddressInterface::PROPERTY_LABEL] ?? null, + ]); + } + + /** + * @return array + */ + protected function materializeAddresses(array $values): array { + $addresses = []; + + foreach ($values as $value) { + $address = $this->materializeAddress($value); + if ($address !== null) { + $addresses[] = $address; + } + } + + return $addresses; + } + + protected function materializeDate(mixed $value): ?DateTimeImmutable { + if ($value instanceof DateTimeImmutable) { + return $value; + } + + if (!is_string($value) || $value === '') { + return null; + } + + try { + return new DateTimeImmutable($value); + } catch (Exception) { + return null; + } + } + + protected function materializePart(mixed $value): ?MessagePartInterface { + if ($value instanceof MessagePartInterface) { + return $value; + } + + if (!is_array($value) || $value === []) { + return null; + } + + return MessagePart::fromArray($value); + } + + /** + * @return array + */ + protected function materializeParts(array $values): array { + $parts = []; + + foreach ($values as $value) { + $part = $this->materializePart($value); + if ($part !== null) { + $parts[] = $part; + } + } + + return $parts; + } + /** * @inheritDoc */ @@ -76,56 +180,56 @@ abstract class MessagePropertiesBaseAbstract extends NodePropertiesBaseAbstract * @inheritDoc */ public function getReceived(): ?DateTimeImmutable { - return $this->data[static::PROPERTY_RECEIVED] ?? null; + return $this->materializeDate($this->data[static::PROPERTY_RECEIVED] ?? null); } /** * @inheritDoc */ public function getSent(): ?DateTimeImmutable { - return $this->data[static::PROPERTY_SENT] ?? null; + return $this->materializeDate($this->data[static::PROPERTY_SENT] ?? null); } /** * @inheritDoc */ public function getSender(): ?AddressInterface { - return $this->data[static::PROPERTY_SENDER] ?? null; + return $this->materializeAddress($this->data[static::PROPERTY_SENDER] ?? null); } /** * @inheritDoc */ public function getFrom(): ?AddressInterface { - return $this->data[static::PROPERTY_FROM] ?? null; + return $this->materializeAddress($this->data[static::PROPERTY_FROM] ?? null); } /** * @inheritDoc */ public function getReplyTo(): array { - return $this->data[static::PROPERTY_REPLY_TO] ?? []; + return $this->materializeAddresses($this->data[static::PROPERTY_REPLY_TO] ?? []); } /** * @inheritDoc */ public function getTo(): array { - return $this->data[static::PROPERTY_TO] ?? []; + return $this->materializeAddresses($this->data[static::PROPERTY_TO] ?? []); } /** * @inheritDoc */ public function getCc(): array { - return $this->data[static::PROPERTY_CC] ?? []; + return $this->materializeAddresses($this->data[static::PROPERTY_CC] ?? []); } /** * @inheritDoc */ public function getBcc(): array { - return $this->data[static::PROPERTY_BCC] ?? []; + return $this->materializeAddresses($this->data[static::PROPERTY_BCC] ?? []); } /** @@ -139,36 +243,38 @@ abstract class MessagePropertiesBaseAbstract extends NodePropertiesBaseAbstract * @inheritDoc */ public function getBody(): ?MessagePartInterface { - return $this->data[static::PROPERTY_BODY] ?? null; + return $this->materializePart($this->data[static::PROPERTY_BODY] ?? null); } /** * @inheritDoc */ public function hasBody(): bool { - return ($this->data[static::PROPERTY_BODY_TEXT_PLAIN] !== null && $this->data[static::PROPERTY_BODY_TEXT_PLAIN] !== '') - || ($this->data[static::PROPERTY_BODY_TEXT_HTML] !== null && $this->data[static::PROPERTY_BODY_TEXT_HTML] !== ''); + $body = $this->getBody(); + + return $this->extractBodyContent($body, 'text/plain') !== null + || $this->extractBodyContent($body, 'text/html') !== null; } /** * @inheritDoc */ public function getBodyTextPlain(): ?string { - return $this->data[static::PROPERTY_BODY_TEXT_PLAIN] ?? null; + return $this->extractBodyContent($this->getBody(), 'text/plain'); } /** * @inheritDoc */ public function getBodyTextHtml(): ?string { - return $this->data[static::PROPERTY_BODY_TEXT_HTML] ?? null; + return $this->extractBodyContent($this->getBody(), 'text/html'); } /** * @inheritDoc */ public function getAttachments(): array { - return $this->data[static::PROPERTY_ATTACHMENTS] ?? []; + return $this->materializeParts($this->data[static::PROPERTY_ATTACHMENTS] ?? []); } /** diff --git a/shared/lib/Mail/Object/MessagePropertiesBaseInterface.php b/shared/lib/Mail/Object/MessagePropertiesBaseInterface.php index 6436c46..cb85118 100644 --- a/shared/lib/Mail/Object/MessagePropertiesBaseInterface.php +++ b/shared/lib/Mail/Object/MessagePropertiesBaseInterface.php @@ -34,8 +34,6 @@ interface MessagePropertiesBaseInterface extends NodePropertiesBaseInterface { public const PROPERTY_BCC = 'bcc'; public const PROPERTY_SUBJECT = 'subject'; public const PROPERTY_BODY = 'body'; - public const PROPERTY_BODY_TEXT_PLAIN = 'bodyTextPlain'; - public const PROPERTY_BODY_TEXT_HTML = 'bodyTextHtml'; public const PROPERTY_ATTACHMENTS = 'attachments'; public const PROPERTY_FLAGS = 'flags'; public const PROPERTY_TAGS = 'tags'; @@ -217,7 +215,7 @@ interface MessagePropertiesBaseInterface extends NodePropertiesBaseInterface { * * @since 2025.05.01 * - * @return array + * @return array */ public function getAttachments(): array; diff --git a/shared/lib/Mail/Object/MessagePropertiesMutableAbstract.php b/shared/lib/Mail/Object/MessagePropertiesMutableAbstract.php index 62a79d6..e384d1b 100644 --- a/shared/lib/Mail/Object/MessagePropertiesMutableAbstract.php +++ b/shared/lib/Mail/Object/MessagePropertiesMutableAbstract.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace KTXF\Mail\Object; use DateTimeImmutable; +use DateTimeInterface; /** * Abstract Message Properties Mutable Class @@ -21,6 +22,90 @@ use DateTimeImmutable; abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbstract implements MessagePropertiesMutableInterface { protected string $type = 'mail.message'; + + /** + * @param array $values + * + * @return array> + */ + protected function normalizeAddresses(array $values): array { + return array_map( + static fn(AddressInterface $value): array => $value->toArray(), + $values, + ); + } + + protected function normalizeDate(?DateTimeImmutable $value): ?string { + return $value?->format(DATE_ATOM); + } + + /** + * @param array $values + * + * @return array> + */ + protected function normalizeParts(array $values): array { + return array_map( + static fn(MessagePartInterface $value): array => $value->jsonSerialize(), + $values, + ); + } + + protected function buildBodyPartFromContent(?string $textPlain, ?string $textHtml): ?MessagePart { + $hasTextPlain = is_string($textPlain) && $textPlain !== ''; + $hasTextHtml = is_string($textHtml) && $textHtml !== ''; + + if (!$hasTextPlain && !$hasTextHtml) { + return null; + } + + if ($hasTextPlain && $hasTextHtml) { + return MessagePart::fromArray([ + 'partId' => 'body', + 'type' => 'multipart/alternative', + 'subParts' => [ + [ + 'partId' => 'body.text', + 'type' => 'text/plain', + 'charset' => 'utf-8', + 'content' => $textPlain, + 'disposition' => 'inline', + ], + [ + 'partId' => 'body.html', + 'type' => 'text/html', + 'charset' => 'utf-8', + 'content' => $textHtml, + 'disposition' => 'inline', + ], + ], + ]); + } + + return MessagePart::fromArray([ + 'partId' => 'body', + 'type' => $hasTextHtml ? 'text/html' : 'text/plain', + 'charset' => 'utf-8', + 'content' => $hasTextHtml ? $textHtml : $textPlain, + 'disposition' => 'inline', + ]); + } + + protected function updateBodyContent(string $type, ?string $value): static { + $currentPlain = $this->getBodyTextPlain(); + $currentHtml = $this->getBodyTextHtml(); + + if ($type === 'text/plain') { + $currentPlain = $value; + } else { + $currentHtml = $value; + } + + $this->data[static::PROPERTY_BODY] = $this->buildBodyPartFromContent($currentPlain, $currentHtml)?->jsonSerialize(); + + return $this; + } + /** * @inheritDoc @@ -91,16 +176,16 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs /** * @inheritDoc */ - public function setReceived(?DateTimeImmutable $value): static { - $this->data[static::PROPERTY_RECEIVED] = $value; + public function setReceived(?DateTimeInterface $value): static { + $this->data[static::PROPERTY_RECEIVED] = $this->normalizeDate($value); return $this; } /** * @inheritDoc */ - public function setSent(DateTimeImmutable $value): static { - $this->data[static::PROPERTY_SENT] = $value; + public function setSent(?DateTimeInterface $value): static { + $this->data[static::PROPERTY_SENT] = $this->normalizeDate($value); return $this; } @@ -108,7 +193,7 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs * @inheritDoc */ public function setSender(?AddressInterface $value): static { - $this->data[static::PROPERTY_SENDER] = $value; + $this->data[static::PROPERTY_SENDER] = $value?->toArray(); return $this; } @@ -116,7 +201,7 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs * @inheritDoc */ public function setFrom(AddressInterface $value): static { - $this->data[static::PROPERTY_FROM] = $value; + $this->data[static::PROPERTY_FROM] = $value->toArray(); return $this; } @@ -124,7 +209,7 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs * @inheritDoc */ public function setReplyTo(AddressInterface ...$value): static { - $this->data[static::PROPERTY_REPLY_TO] = $value; + $this->data[static::PROPERTY_REPLY_TO] = $this->normalizeAddresses($value); return $this; } @@ -132,7 +217,7 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs * @inheritDoc */ public function setTo(AddressInterface ...$value): static { - $this->data[static::PROPERTY_TO] = $value; + $this->data[static::PROPERTY_TO] = $this->normalizeAddresses($value); return $this; } @@ -140,7 +225,7 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs * @inheritDoc */ public function setCc(AddressInterface ...$value): static { - $this->data[static::PROPERTY_CC] = $value; + $this->data[static::PROPERTY_CC] = $this->normalizeAddresses($value); return $this; } @@ -148,7 +233,7 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs * @inheritDoc */ public function setBcc(AddressInterface ...$value): static { - $this->data[static::PROPERTY_BCC] = $value; + $this->data[static::PROPERTY_BCC] = $this->normalizeAddresses($value); return $this; } @@ -164,7 +249,7 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs * @inheritDoc */ public function setBody(?MessagePartInterface $value): static { - $this->data[static::PROPERTY_BODY] = $value; + $this->data[static::PROPERTY_BODY] = $value?->jsonSerialize(); return $this; } @@ -172,31 +257,29 @@ abstract class MessagePropertiesMutableAbstract extends MessagePropertiesBaseAbs * @inheritDoc */ public function setBodyTextPlain(?string $value): static { - $this->data[static::PROPERTY_BODY_TEXT_PLAIN] = $value; - return $this; + return $this->updateBodyContent('text/plain', $value); } /** * @inheritDoc */ public function setBodyTextHtml(?string $value): static { - $this->data[static::PROPERTY_BODY_TEXT_HTML] = $value; + return $this->updateBodyContent('text/html', $value); + } + + /** + * @inheritDoc + */ + public function setAttachments(\KTXF\Mail\Object\MessagePartInterface ...$value): static { + $this->data[static::PROPERTY_ATTACHMENTS] = $this->normalizeParts($value); return $this; } /** * @inheritDoc */ - public function setAttachments(AttachmentInterface ...$value): static { - $this->data[static::PROPERTY_ATTACHMENTS] = $value; - return $this; - } - - /** - * @inheritDoc - */ - public function addAttachment(AttachmentInterface $value): static { - $this->data[static::PROPERTY_ATTACHMENTS][] = $value; + public function addAttachment(\KTXF\Mail\Object\MessagePartInterface $value): static { + $this->data[static::PROPERTY_ATTACHMENTS][] = $value->jsonSerialize(); return $this; } diff --git a/shared/lib/Mail/Object/MessagePropertiesMutableInterface.php b/shared/lib/Mail/Object/MessagePropertiesMutableInterface.php index 4e880ce..4fcce01 100644 --- a/shared/lib/Mail/Object/MessagePropertiesMutableInterface.php +++ b/shared/lib/Mail/Object/MessagePropertiesMutableInterface.php @@ -223,22 +223,22 @@ interface MessagePropertiesMutableInterface extends MessagePropertiesBaseInterfa * * @since 2025.05.01 * - * @param AttachmentInterface ...$value + * @param MessagePartInterface ...$value * * @return self */ - public function setAttachments(AttachmentInterface ...$value): static; + public function setAttachments(MessagePartInterface ...$value): static; /** * Adds an attachment * * @since 2025.05.01 * - * @param AttachmentInterface $value + * @param MessagePartInterface $value * * @return self */ - public function addAttachment(AttachmentInterface $value): static; + public function addAttachment(MessagePartInterface $value): static; /** * Sets message flags diff --git a/shared/lib/Mail/Service/ServiceEntitySubmitInterface.php b/shared/lib/Mail/Service/ServiceEntitySubmitInterface.php new file mode 100644 index 0000000..76349b3 --- /dev/null +++ b/shared/lib/Mail/Service/ServiceEntitySubmitInterface.php @@ -0,0 +1,54 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Mail\Service; + +use KTXF\Mail\Entity\EntityMutableInterface; +use KTXF\Mail\Object\AddressInterface; +use KTXF\Mail\Object\MessagePropertiesMutableInterface; +use KTXF\Mail\Submission\EntitySubmitResult; +use KTXF\Resource\Identifier\EntityIdentifierInterface; + +/** + * Mail Service Submit Interface + * + * Interface for mail services capable of outbound submission. + * Supports submission of fresh in-memory messages and already-existing + * provider-backed drafts through a single provider-agnostic contract. + * + * @since 2026.06.07 + */ +interface ServiceEntitySubmitInterface { + + public const CAPABILITY_ENTITY_SUBMIT_FRESH = 'EntitySubmitFresh'; + public const CAPABILITY_ENTITY_SUBMIT_DRAFT = 'EntitySubmitDraft'; + + /** + * Creates a fresh entity instance for composition + * + * @since 2025.05.01 + * + * @return EntityMutableInterface Fresh entity object + */ + public function entityFresh(): EntityMutableInterface; + + /** + * Submits an outbound message. + * + * @since 2026.06.07 + * + * @param AddressInterface $sender Sender address + * @param EntityIdentifierInterface|null $source Source entity identifier + * @param MessagePropertiesMutableInterface|null $message Message properties + * + * @return EntitySubmitResult Normalized submission result + */ + public function entitySubmit(AddressInterface $sender, EntityIdentifierInterface|null $source = null, MessagePropertiesMutableInterface|null $message = null): EntitySubmitResult; + +} \ No newline at end of file diff --git a/shared/lib/Mail/Service/ServiceEntityTransmitInterface.php b/shared/lib/Mail/Service/ServiceEntityTransmitInterface.php deleted file mode 100644 index 23b0158..0000000 --- a/shared/lib/Mail/Service/ServiceEntityTransmitInterface.php +++ /dev/null @@ -1,48 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXF\Mail\Service; - -use KTXF\Mail\Entity\EntityMutableInterface; -use KTXF\Mail\Exception\SendException; - -/** - * Mail Service Transmit Interface - * - * Interface for mail services capable of transmitting outbound entities. - * - * @since 2025.05.01 - */ -interface ServiceEntityTransmitInterface { - - public const CAPABILITY_ENTITY_TRANSMIT = 'EntityTransmit'; - - /** - * Creates a fresh entity instance for composition - * - * @since 2025.05.01 - * - * @return EntityMutableInterface Fresh entity object - */ - public function entityFresh(): EntityMutableInterface; - - /** - * Transmits an outbound entity - * - * @since 2025.05.01 - * - * @param EntityMutableInterface $entity Entity to transmit - * - * @return string Entity identifier assigned by the transport - * - * @throws SendException On delivery failure - */ - public function entitySend(EntityMutableInterface $entity): string; - -} diff --git a/shared/lib/Mail/Submission/EntitySubmitResult.php b/shared/lib/Mail/Submission/EntitySubmitResult.php new file mode 100644 index 0000000..fcead54 --- /dev/null +++ b/shared/lib/Mail/Submission/EntitySubmitResult.php @@ -0,0 +1,52 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Mail\Submission; + +use InvalidArgumentException; +use JsonSerializable; +use KTXF\Resource\Identifier\EntityIdentifier; + +/** + * Entity Submit Result + * + * Normalized result for outbound submission. + * + * @since 2026.06.07 + */ +final class EntitySubmitResult implements JsonSerializable { + + public const DISPOSITION_SENT = 'sent'; + public const DISPOSITION_ERROR = 'error'; + + public function __construct( + public readonly string $disposition, + public readonly ?string $transportId = null, + public readonly ?EntityIdentifier $sourceDraft = null, + public readonly ?EntityIdentifier $sentEntity = null, + public readonly ?string $errorCode = null, + public readonly ?string $errorMessage = null, + ) { + if (!in_array($this->disposition, [self::DISPOSITION_SENT, self::DISPOSITION_ERROR], true)) { + throw new InvalidArgumentException('Invalid submission disposition: ' . $this->disposition); + } + } + + public function jsonSerialize(): array { + return array_filter([ + 'disposition' => $this->disposition, + 'transportId' => $this->transportId, + 'sourceDraft' => $this->sourceDraft !== null ? (string)$this->sourceDraft : null, + 'sentEntity' => $this->sentEntity !== null ? (string)$this->sentEntity : null, + 'errorCode' => $this->errorCode, + 'errorMessage' => $this->errorMessage, + ], static fn(mixed $value): bool => $value !== null); + } + +} \ No newline at end of file -- 2.39.5