refactor: message object properties
JS Unit Tests / test (pull_request) Successful in 23s
Build Test / build (pull_request) Successful in 26s
PHP Unit Tests / test (pull_request) Successful in 52s

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-16 13:12:30 -04:00
parent 6dadd13acf
commit 869f2e4fb4
15 changed files with 506 additions and 406 deletions
+20 -13
View File
@@ -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
*/
+9 -2
View File
@@ -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
-194
View File
@@ -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;
}
+37
View File
@@ -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);
}
}