Merge pull request 'refactor: message object properties' (#102) from refactor/message-object-properties into main
Reviewed-on: #102
This commit was merged in pull request #102.
This commit is contained in:
@@ -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<address: string, label: string|null> $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
|
||||
*/
|
||||
|
||||
@@ -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<address: string, label: string|null>
|
||||
*/
|
||||
public function toArray(): array;
|
||||
|
||||
/**
|
||||
* Gets the email address
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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<int,AddressInterface>
|
||||
*/
|
||||
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<int,MessagePartInterface>
|
||||
*/
|
||||
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] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<int,AttachmentInterface>
|
||||
* @return array<int,MessagePartInterface>
|
||||
*/
|
||||
public function getAttachments(): array;
|
||||
|
||||
|
||||
@@ -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<int,AddressInterface> $values
|
||||
*
|
||||
* @return array<int,array<string,string|null>>
|
||||
*/
|
||||
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<int,MessagePartInterface> $values
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user