generated from Nodarx/template
feat: initial version
Signed-off-by: Sebastian Krupinski <root@LAPTOP-7DVOR6NC>
This commit was merged in pull request #1.
This commit is contained in:
93
lib/Providers/CollectionProperties.php
Normal file
93
lib/Providers/CollectionProperties.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use Gricob\IMAP\Mailbox;
|
||||
use KTXF\Mail\Collection\CollectionPropertiesMutableAbstract;
|
||||
use KTXF\Mail\Collection\CollectionRoles;
|
||||
|
||||
/**
|
||||
* IMAP Mail Collection Properties
|
||||
*
|
||||
* Backed by the same internal $data shape as the JMAP provider so that cache
|
||||
* documents are interchangeable with fromStore() / toStore().
|
||||
*/
|
||||
class CollectionProperties extends CollectionPropertiesMutableAbstract
|
||||
{
|
||||
// ── IMAP hydration ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Populate from a gricob Mailbox object.
|
||||
*
|
||||
* Total / unread counts are NOT available from a LIST response alone.
|
||||
* They must be set separately (after SELECT + SEARCH UNSEEN).
|
||||
*/
|
||||
public function fromImap(Mailbox $mailbox): static
|
||||
{
|
||||
$this->data['label'] = $mailbox->name;
|
||||
$this->data['delimiter'] = $mailbox->hierarchyDelimiter;
|
||||
$this->data['attributes'] = $mailbox->nameAttributes;
|
||||
$this->data['subscribed'] = in_array('\Subscribed', $mailbox->nameAttributes, true);
|
||||
$this->data['total'] = 0;
|
||||
$this->data['unread'] = 0;
|
||||
$this->data['rank'] = 0;
|
||||
|
||||
// Map standard IMAP role attributes
|
||||
$this->data['role'] = $this->roleFromAttributes($mailbox->nameAttributes)->value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── Store (MongoDB cache) ────────────────────────────────────────────────
|
||||
|
||||
public function toStore(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
{
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── JSON helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function getDelimiter(): ?string
|
||||
{
|
||||
return $this->data['delimiter'] ?? null;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private function roleFromAttributes(array $attributes): CollectionRoles
|
||||
{
|
||||
foreach ($attributes as $attr) {
|
||||
$lower = strtolower($attr);
|
||||
$role = match ($lower) {
|
||||
'\sent' => CollectionRoles::Sent,
|
||||
'\trash' => CollectionRoles::Trash,
|
||||
'\drafts' => CollectionRoles::Drafts,
|
||||
'\junk' => CollectionRoles::Junk,
|
||||
'\archive' => CollectionRoles::Archive,
|
||||
default => null,
|
||||
};
|
||||
if ($role !== null) {
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
return CollectionRoles::Custom;
|
||||
}
|
||||
}
|
||||
101
lib/Providers/CollectionResource.php
Normal file
101
lib/Providers/CollectionResource.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use Gricob\IMAP\Mailbox;
|
||||
use KTXF\Mail\Collection\CollectionMutableAbstract;
|
||||
|
||||
/**
|
||||
* IMAP Mail Collection Resource
|
||||
*
|
||||
* Represents one IMAP mailbox / folder.
|
||||
*/
|
||||
class CollectionResource extends CollectionMutableAbstract
|
||||
{
|
||||
public function __construct(
|
||||
string $provider = 'imap',
|
||||
string|int|null $service = null,
|
||||
) {
|
||||
parent::__construct($provider, $service);
|
||||
}
|
||||
|
||||
// ── IMAP hydration ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Populate from a gricob Mailbox object.
|
||||
*
|
||||
* @param Mailbox $mailbox gricob Mailbox value object from LIST response
|
||||
*/
|
||||
public function fromImap(Mailbox $mailbox): static
|
||||
{
|
||||
// The mailbox name is its unique identifier within the account
|
||||
$this->data['identifier'] = $mailbox->name;
|
||||
|
||||
// Derive parent collection from path + delimiter
|
||||
$delimiter = $mailbox->hierarchyDelimiter;
|
||||
if ($delimiter && str_contains($mailbox->name, $delimiter)) {
|
||||
$parts = explode($delimiter, $mailbox->name);
|
||||
array_pop($parts);
|
||||
$this->data['collection'] = implode($delimiter, $parts);
|
||||
} else {
|
||||
$this->data['collection'] = null;
|
||||
}
|
||||
|
||||
$this->getProperties()->fromImap($mailbox);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── Store (MongoDB cache) ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Serialise to a MongoDB document.
|
||||
*
|
||||
* The caller must inject the service UUID as `sid` before persisting.
|
||||
*/
|
||||
public function toStore(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->data,
|
||||
[
|
||||
'name' => $this->data['identifier'],
|
||||
'properties' => $this->getProperties()->toStore(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
{
|
||||
$this->data = $data;
|
||||
if (isset($data['properties'])) {
|
||||
$this->getProperties()->fromStore($data['properties']);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── Getters (lazy properties init) ───────────────────────────────────────
|
||||
|
||||
public function getProperties(): CollectionProperties
|
||||
{
|
||||
if (!isset($this->properties)) {
|
||||
$this->properties = new CollectionProperties([]);
|
||||
}
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
// ── JSON ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = $this->data;
|
||||
$data['properties'] = $this->getProperties()->jsonSerialize();
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
90
lib/Providers/EntityResource.php
Normal file
90
lib/Providers/EntityResource.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Gricob\IMAP\Mime\Part\Part;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\FetchData;
|
||||
use KTXF\Mail\Entity\EntityMutableAbstract;
|
||||
|
||||
/**
|
||||
* Mail Entity Resource Implementation
|
||||
*/
|
||||
class EntityResource extends EntityMutableAbstract {
|
||||
|
||||
public function __construct(
|
||||
string $provider = 'imap',
|
||||
string|int|null $service = null,
|
||||
) {
|
||||
parent::__construct($provider, $service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert gricob FetchData to mail entity object
|
||||
*
|
||||
* @param FetchData $fetchData result from IMAP FETCH command
|
||||
* @param string $mailbox IMAP mailbox name (used as collection)
|
||||
* @param Part|null $bodyPart MIME Part tree for body content (optional)
|
||||
*/
|
||||
public function fromImap(FetchData $fetchData, string $mailbox, ?Part $bodyPart = null): static {
|
||||
|
||||
// Collection = the IMAP mailbox name
|
||||
$this->data['collection'] = $mailbox;
|
||||
|
||||
// Identifier = UID (preferred) or sequence number as fallback
|
||||
$this->data['identifier'] = $fetchData->uid ?? $fetchData->id;
|
||||
|
||||
// Created = INTERNALDATE (server arrival time)
|
||||
if ($fetchData->internalDate !== null) {
|
||||
$this->data['created'] = $fetchData->internalDate->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
$this->getProperties()->fromImap(
|
||||
flags: $fetchData->flags ?? [],
|
||||
envelope: $fetchData->envelope,
|
||||
bodyStructure: $fetchData->bodyStructure,
|
||||
size: $fetchData->rfc822Size ?? 0,
|
||||
bodyPart: $bodyPart,
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert mail entity object to store array
|
||||
*/
|
||||
public function toStore(): array {
|
||||
return array_merge(
|
||||
$this->data,
|
||||
['properties' => $this->getProperties()->toStore()]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate mail entity object from store array
|
||||
*/
|
||||
public function fromStore(array $data): static {
|
||||
$properties = $data['properties'] ?? [];
|
||||
unset($data['properties']);
|
||||
$this->data = $data;
|
||||
$this->getProperties()->fromStore($properties);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getProperties(): MessageProperties {
|
||||
if (!isset($this->properties)) {
|
||||
$this->properties = new MessageProperties([]);
|
||||
}
|
||||
return $this->properties;
|
||||
}
|
||||
}
|
||||
128
lib/Providers/MessageAttachment.php
Normal file
128
lib/Providers/MessageAttachment.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
/**
|
||||
* Mail Attachment Object
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class MessageAttachment implements \KTXF\Mail\Object\MessagePartInterface {
|
||||
|
||||
protected MessagePart $_meta;
|
||||
protected ?string $_contents = null;
|
||||
|
||||
public function __construct(?MessagePart $meta = null, ?string $contents = null) {
|
||||
if ($meta === null) {
|
||||
$meta = new MessagePart();
|
||||
$meta->setDisposition('attachment');
|
||||
$meta->setType('application/octet-stream');
|
||||
}
|
||||
$this->setParameters($meta);
|
||||
if ($contents !== null) {
|
||||
$this->setContents($contents);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attachment parameters
|
||||
*/
|
||||
public function setParameters(?MessagePart $meta): static {
|
||||
$this->_meta = $meta;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attachment parameters
|
||||
*/
|
||||
public function getParameters(): MessagePart {
|
||||
return $this->_meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique identifier for this attachment (MIME part id)
|
||||
*/
|
||||
public function id(): string {
|
||||
return $this->_meta->getBlobId() ?? $this->_meta->getId() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attachment file name
|
||||
*/
|
||||
public function setName(string $value): static {
|
||||
$this->_meta->setName($value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attachment file name
|
||||
*/
|
||||
public function getName(): ?string {
|
||||
return $this->_meta->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attachment MIME type
|
||||
*/
|
||||
public function setType(string $value): static {
|
||||
$this->_meta->setType($value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attachment MIME type
|
||||
*/
|
||||
public function getType(): ?string {
|
||||
return $this->_meta->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the attachment contents (binary data)
|
||||
*/
|
||||
public function setContents(string $value): static {
|
||||
$this->_contents = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attachment contents
|
||||
*/
|
||||
public function getContents(): ?string {
|
||||
return $this->_contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the attachment is embedded (inline)
|
||||
*/
|
||||
public function setEmbedded(bool $value): static {
|
||||
$this->_meta->setDisposition($value ? 'inline' : 'attachment');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the attachment is embedded (inline)
|
||||
*/
|
||||
public function getEmbedded(): bool {
|
||||
return $this->_meta->getDisposition() === 'inline';
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// MessagePartInterface pass-throughs
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function getBlobId(): ?string { return $this->_meta->getBlobId(); }
|
||||
public function getId(): ?string { return $this->_meta->getId(); }
|
||||
public function getDisposition(): ?string { return $this->_meta->getDisposition(); }
|
||||
public function getCharset(): ?string { return $this->_meta->getCharset(); }
|
||||
public function getLanguage(): ?string { return $this->_meta->getLanguage(); }
|
||||
public function getLocation(): ?string { return $this->_meta->getLocation(); }
|
||||
public function getParts(): array { return $this->_meta->getParts(); }
|
||||
public function jsonSerialize(): array { return $this->_meta->jsonSerialize(); }
|
||||
}
|
||||
176
lib/Providers/MessagePart.php
Normal file
176
lib/Providers/MessagePart.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\MultiPart;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\Part;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\SinglePart;
|
||||
use KTXF\Mail\Object\MessagePartMutableAbstract;
|
||||
|
||||
/**
|
||||
* Mail Message Part Implementation
|
||||
*/
|
||||
class MessagePart extends MessagePartMutableAbstract {
|
||||
|
||||
/**
|
||||
* Convert gricob BodyStructure part to message part object
|
||||
*
|
||||
* @param Part $part gricob BodyStructure Part (SinglePart or MultiPart)
|
||||
* @param string $partId numeric part identifier (e.g. "1", "1.1", "2")
|
||||
*/
|
||||
public function fromImap(Part $part, string $partId = '1'): static {
|
||||
|
||||
$this->data['partId'] = $partId;
|
||||
|
||||
if ($part instanceof SinglePart) {
|
||||
$mimeType = strtolower($part->type) . '/' . strtolower($part->subtype);
|
||||
$this->data['type'] = $mimeType;
|
||||
|
||||
if ($part->id !== null) {
|
||||
$this->data['blobId'] = trim($part->id, '<>');
|
||||
}
|
||||
|
||||
// Content-Type parameters (name, charset, etc.)
|
||||
if (!empty($part->attributes)) {
|
||||
foreach ($part->attributes as $key => $value) {
|
||||
$keyLower = strtolower($key);
|
||||
if ($keyLower === 'name') {
|
||||
$this->data['name'] = $value;
|
||||
} elseif ($keyLower === 'charset') {
|
||||
$this->data['charset'] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($part->encoding !== null) {
|
||||
$this->data['encoding'] = strtolower($part->encoding);
|
||||
}
|
||||
|
||||
if ($part->size !== null) {
|
||||
$this->data['size'] = $part->size;
|
||||
}
|
||||
|
||||
if ($part->disposition !== null) {
|
||||
$this->data['disposition'] = strtolower($part->disposition->type);
|
||||
// disposition filename attribute
|
||||
if (!empty($part->disposition->attributes)) {
|
||||
foreach ($part->disposition->attributes as $key => $value) {
|
||||
if (strtolower($key) === 'filename') {
|
||||
$this->data['name'] = $this->data['name'] ?? $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($part->language)) {
|
||||
$this->data['language'] = implode(',', $part->language);
|
||||
}
|
||||
|
||||
if ($part->location !== null) {
|
||||
$this->data['location'] = $part->location;
|
||||
}
|
||||
|
||||
} elseif ($part instanceof MultiPart) {
|
||||
$this->data['type'] = 'multipart/' . strtolower($part->subtype);
|
||||
|
||||
if ($part->disposition !== null) {
|
||||
$this->data['disposition'] = strtolower($part->disposition->type);
|
||||
}
|
||||
|
||||
if (!empty($part->language)) {
|
||||
$this->data['language'] = implode(',', $part->language);
|
||||
}
|
||||
|
||||
if ($part->location !== null) {
|
||||
$this->data['location'] = $part->location;
|
||||
}
|
||||
|
||||
// Recursively process sub-parts
|
||||
foreach ($part->parts as $index => $subPart) {
|
||||
$subPartId = $partId . '.' . ($index + 1);
|
||||
$this->parts[] = (new MessagePart())->fromImap($subPart, $subPartId);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert message part to store array
|
||||
*/
|
||||
public function toStore(): array {
|
||||
$data = $this->data;
|
||||
if (count($this->parts) > 0) {
|
||||
$data['subParts'] = [];
|
||||
foreach ($this->parts as $subPart) {
|
||||
if ($subPart instanceof MessagePart) {
|
||||
$data['subParts'][] = $subPart->toStore();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$data['subParts'] = null;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate message part from store array
|
||||
*/
|
||||
public function fromStore(array $data): static {
|
||||
if (isset($data['subParts']) && is_array($data['subParts'])) {
|
||||
foreach ($data['subParts'] as $subPart) {
|
||||
$this->parts[] = (new MessagePart())->fromStore($subPart);
|
||||
}
|
||||
unset($data['subParts']);
|
||||
}
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject decoded body content from a parallel gricob Mime Part tree.
|
||||
*
|
||||
* Walks the gricob Mime Part tree alongside this MessagePart tree and
|
||||
* sets 'content' on each leaf single-part node from its decoded body.
|
||||
*
|
||||
* @param \Gricob\IMAP\Mime\Part\Part $mimePart Corresponding gricob Mime Part node
|
||||
*/
|
||||
public function injectBodyContent(\Gricob\IMAP\Mime\Part\Part $mimePart): void
|
||||
{
|
||||
if ($mimePart instanceof \Gricob\IMAP\Mime\Part\MultiPart) {
|
||||
foreach ($mimePart->parts as $index => $childMimePart) {
|
||||
$childPart = $this->parts[$index] ?? null;
|
||||
if ($childPart instanceof MessagePart) {
|
||||
$childPart->injectBodyContent($childMimePart);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($mimePart instanceof \Gricob\IMAP\Mime\Part\SinglePart) {
|
||||
// Only inject content for text/* parts; binary parts (images, PDFs, …)
|
||||
// produce raw bytes that cannot be JSON-encoded as UTF-8 strings.
|
||||
$type = strtolower($this->data['type'] ?? '');
|
||||
if (!str_starts_with($type, 'text/')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$decoded = $mimePart->decodedBody();
|
||||
} catch (\Throwable) {
|
||||
return;
|
||||
}
|
||||
if ($decoded !== null && $decoded !== '') {
|
||||
$charset = $mimePart->charset() ?? 'utf-8';
|
||||
$this->data['content'] = MessageProperties::toUtf8($decoded, $charset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
220
lib/Providers/MessageProperties.php
Normal file
220
lib/Providers/MessageProperties.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Gricob\IMAP\Mime\Part\Part as MimePart;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\MultiPart;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\Part;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\SinglePart;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\Envelope;
|
||||
use KTXF\Mail\Object\MessagePropertiesMutableAbstract;
|
||||
|
||||
/**
|
||||
* Mail Message Properties Implementation
|
||||
*/
|
||||
class MessageProperties extends MessagePropertiesMutableAbstract {
|
||||
|
||||
/**
|
||||
* Convert IMAP data to mail message properties object
|
||||
*
|
||||
* @param array $flags IMAP flags (e.g. ['\Seen', '\Flagged', ...])
|
||||
* @param ?Envelope $envelope parsed envelope from gricob
|
||||
* @param ?BodyStructure $bodyStructure parsed body structure from gricob
|
||||
* @param int $size RFC822.SIZE byte count
|
||||
*/
|
||||
public function fromImap(
|
||||
array $flags,
|
||||
?Envelope $envelope,
|
||||
?BodyStructure $bodyStructure,
|
||||
int $size = 0,
|
||||
?MimePart $bodyPart = null,
|
||||
): static {
|
||||
|
||||
// ── Size ──────────────────────────────────────────────────────
|
||||
$this->data['size'] = $size;
|
||||
|
||||
// ── Flags ─────────────────────────────────────────────────────
|
||||
$this->data['flags'] = [];
|
||||
foreach ($flags as $flag) {
|
||||
$flag = ltrim($flag, '\\');
|
||||
$normalized = match (strtolower($flag)) {
|
||||
'seen' => 'read',
|
||||
'flagged' => 'flagged',
|
||||
'answered' => 'answered',
|
||||
'draft' => 'draft',
|
||||
'deleted' => 'deleted',
|
||||
default => strtolower($flag),
|
||||
};
|
||||
$this->data['flags'][$normalized] = true;
|
||||
}
|
||||
|
||||
// ── Envelope ──────────────────────────────────────────────────
|
||||
if ($envelope !== null) {
|
||||
|
||||
if ($envelope->messageId !== null) {
|
||||
$this->data['urid'] = trim($envelope->messageId, '<>');
|
||||
}
|
||||
|
||||
if ($envelope->subject !== null) {
|
||||
// Decode MIME encoded-word in subject
|
||||
$this->data['subject'] = mb_decode_mimeheader($envelope->subject);
|
||||
}
|
||||
|
||||
if ($envelope->date !== null) {
|
||||
$date = $envelope->date instanceof DateTimeImmutable
|
||||
? $envelope->date
|
||||
: new DateTimeImmutable($envelope->date);
|
||||
$this->data['date'] = $date->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
if ($envelope->inReplyTo !== null) {
|
||||
$this->data['inReplyTo'] = $envelope->inReplyTo;
|
||||
}
|
||||
|
||||
$addressToArray = static function ($addr): array {
|
||||
$email = '';
|
||||
if ($addr->mailboxName !== null && $addr->hostName !== null) {
|
||||
$email = $addr->mailboxName . '@' . $addr->hostName;
|
||||
} elseif ($addr->mailboxName !== null) {
|
||||
$email = $addr->mailboxName;
|
||||
}
|
||||
return [
|
||||
'address' => $email,
|
||||
'label' => $addr->displayName ?? null,
|
||||
];
|
||||
};
|
||||
|
||||
if (!empty($envelope->from)) {
|
||||
$this->data['from'] = $addressToArray($envelope->from[0]);
|
||||
}
|
||||
|
||||
if (!empty($envelope->sender)) {
|
||||
$this->data['sender'] = $addressToArray($envelope->sender[0]);
|
||||
}
|
||||
|
||||
foreach (['to', 'cc', 'bcc', 'replyTo'] as $field) {
|
||||
$envField = $field === 'replyTo' ? 'replyTo' : $field;
|
||||
if (!empty($envelope->$envField)) {
|
||||
$this->data[$field] = [];
|
||||
foreach ($envelope->$envField as $addr) {
|
||||
$this->data[$field][] = $addressToArray($addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Body Structure ────────────────────────────────────────────
|
||||
if ($bodyStructure !== null) {
|
||||
$rootPart = (new MessagePart())->fromImap($bodyStructure->part, '1');
|
||||
|
||||
// ── Body Content: inject decoded content onto part nodes ──────
|
||||
if ($bodyPart !== null) {
|
||||
$rootPart->injectBodyContent($bodyPart);
|
||||
}
|
||||
|
||||
$this->data['body'] = $rootPart->toStore();
|
||||
|
||||
// Collect attachments: non-body parts with name or attachment disposition
|
||||
$attachments = [];
|
||||
self::collectAttachments($bodyStructure->part, '1', $attachments);
|
||||
if (!empty($attachments)) {
|
||||
$this->data['attachments'] = $attachments;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to UTF-8 from the given charset.
|
||||
*
|
||||
* Tries mb_convert_encoding first; falls back to iconv when mbstring does
|
||||
* not recognise the charset name (e.g. "windows-1250").
|
||||
*/
|
||||
public static function toUtf8(string $content, string $charset): string
|
||||
{
|
||||
if ($charset === '' || in_array(strtolower($charset), ['utf-8', 'utf8'], true)) {
|
||||
// Content claims to be UTF-8 but may still have invalid sequences; scrub to be safe.
|
||||
return mb_convert_encoding($content, 'UTF-8', 'UTF-8');
|
||||
}
|
||||
// Try mbstring first
|
||||
try {
|
||||
$converted = mb_convert_encoding($content, 'UTF-8', $charset);
|
||||
if ($converted !== false) {
|
||||
return $converted;
|
||||
}
|
||||
} catch (\ValueError) {
|
||||
// charset not recognised by mbstring — fall through to iconv
|
||||
}
|
||||
// iconv fallback (handles Windows-125x, ISO-8859-*, etc.)
|
||||
$converted = @iconv($charset, 'UTF-8//TRANSLIT//IGNORE', $content);
|
||||
$content = ($converted !== false) ? $converted : $content;
|
||||
// Final scrub: strip any residual invalid UTF-8 bytes so json_encode never fails.
|
||||
return mb_convert_encoding($content, 'UTF-8', 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect attachment parts from body structure
|
||||
*/
|
||||
private static function collectAttachments(
|
||||
Part $part,
|
||||
string $partId,
|
||||
array &$attachments,
|
||||
): void {
|
||||
if ($part instanceof SinglePart) {
|
||||
$type = strtolower($part->type ?? '');
|
||||
$subtype = strtolower($part->subtype ?? '');
|
||||
$disposition = strtolower($part->disposition?->type ?? '');
|
||||
$name = null;
|
||||
if (!empty($part->attributes)) {
|
||||
foreach ($part->attributes as $k => $v) {
|
||||
if (strtolower($k) === 'name') {
|
||||
$name = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($part->disposition?->attributes)) {
|
||||
foreach ($part->disposition->attributes as $k => $v) {
|
||||
if (strtolower($k) === 'filename') {
|
||||
$name = $name ?? $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
$isInlineText = ($type === 'text' && ($subtype === 'plain' || $subtype === 'html') && $disposition !== 'attachment');
|
||||
if (!$isInlineText && ($disposition !== '' || $name !== null)) {
|
||||
$mp = (new MessagePart())->fromImap($part, $partId);
|
||||
$attachments[] = $mp->toStore();
|
||||
}
|
||||
} elseif ($part instanceof MultiPart) {
|
||||
foreach ($part->parts as $index => $subPart) {
|
||||
self::collectAttachments($subPart, $partId . '.' . ($index + 1), $attachments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize to store array
|
||||
*/
|
||||
public function toStore(): array {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate from store array
|
||||
*/
|
||||
public function fromStore(array $data): static {
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
246
lib/Providers/Provider.php
Normal file
246
lib/Providers/Provider.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use KTXF\Mail\Provider\ProviderBaseInterface;
|
||||
use KTXF\Mail\Provider\ProviderServiceDiscoverInterface;
|
||||
use KTXF\Mail\Provider\ProviderServiceMutateInterface;
|
||||
use KTXF\Mail\Provider\ProviderServiceTestInterface;
|
||||
use KTXF\Mail\Service\ServiceBaseInterface;
|
||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||
use KTXF\Resource\Provider\ResourceServiceMutateInterface;
|
||||
use KTXM\ProviderImapMail\Service\Discovery;
|
||||
use KTXM\ProviderImapMail\Service\Remote\RemoteService;
|
||||
use KTXM\ProviderImapMail\Stores\ServiceStore;
|
||||
|
||||
/**
|
||||
* IMAP Mail Provider
|
||||
*
|
||||
* Registers IMAP as a mail provider and handles service lifecycle:
|
||||
* list / fetch / create / modify / destroy / discover / test.
|
||||
*/
|
||||
class Provider implements ProviderServiceMutateInterface, ProviderServiceDiscoverInterface, ProviderServiceTestInterface
|
||||
{
|
||||
public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE;
|
||||
|
||||
protected const PROVIDER_IDENTIFIER = 'imap';
|
||||
protected const PROVIDER_LABEL = 'IMAP Mail Provider';
|
||||
protected const PROVIDER_DESCRIPTION = 'Provides mail services via the IMAP protocol';
|
||||
protected const PROVIDER_ICON = 'mdi-email';
|
||||
|
||||
protected array $providerAbilities = [
|
||||
self::CAPABILITY_SERVICE_LIST => true,
|
||||
self::CAPABILITY_SERVICE_FETCH => true,
|
||||
self::CAPABILITY_SERVICE_EXTANT => true,
|
||||
self::CAPABILITY_SERVICE_CREATE => true,
|
||||
self::CAPABILITY_SERVICE_MODIFY => true,
|
||||
self::CAPABILITY_SERVICE_DESTROY => true,
|
||||
self::CAPABILITY_SERVICE_TEST => true,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ServiceStore $serviceStore,
|
||||
) {}
|
||||
|
||||
// ── ProviderBaseInterface ─────────────────────────────────────────────────
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
self::JSON_PROPERTY_TYPE => self::JSON_TYPE,
|
||||
self::JSON_PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER,
|
||||
self::JSON_PROPERTY_LABEL => self::PROVIDER_LABEL,
|
||||
self::JSON_PROPERTY_CAPABILITIES => $this->providerAbilities,
|
||||
];
|
||||
}
|
||||
|
||||
public function jsonDeserialize(array|string $data): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return self::TYPE_MAIL;
|
||||
}
|
||||
|
||||
public function identifier(): string
|
||||
{
|
||||
return self::PROVIDER_IDENTIFIER;
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return self::PROVIDER_LABEL;
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return self::PROVIDER_DESCRIPTION;
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return self::PROVIDER_ICON;
|
||||
}
|
||||
|
||||
public function capable(string $value): bool
|
||||
{
|
||||
return !empty($this->providerAbilities[$value]);
|
||||
}
|
||||
|
||||
public function capabilities(): array
|
||||
{
|
||||
return $this->providerAbilities;
|
||||
}
|
||||
|
||||
// ── ProviderServiceMutateInterface ────────────────────────────────────────
|
||||
|
||||
public function serviceList(string $tenantId, string $userId, array $filter = []): array
|
||||
{
|
||||
$list = $this->serviceStore->list($tenantId, $userId, $filter);
|
||||
$result = [];
|
||||
foreach ($list as $entry) {
|
||||
$service = new Service();
|
||||
$service->fromStore($entry);
|
||||
$result[$service->identifier()] = $service;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?Service
|
||||
{
|
||||
return $this->serviceStore->fetch($tenantId, $userId, $identifier);
|
||||
}
|
||||
|
||||
public function serviceFindByAddress(string $tenantId, string $userId, string $address): ?Service
|
||||
{
|
||||
/** @var Service[] $services */
|
||||
$services = $this->serviceList($tenantId, $userId);
|
||||
foreach ($services as $service) {
|
||||
if ($service->hasAddress($address)) {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function serviceExtant(string $tenantId, string $userId, string|int ...$identifiers): array
|
||||
{
|
||||
return $this->serviceStore->extant($tenantId, $userId, $identifiers);
|
||||
}
|
||||
|
||||
public function serviceFresh(): ResourceServiceMutateInterface
|
||||
{
|
||||
return new Service();
|
||||
}
|
||||
|
||||
public function serviceCreate(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string
|
||||
{
|
||||
if (!($service instanceof Service)) {
|
||||
throw new \InvalidArgumentException('Service must be an instance of IMAP Service');
|
||||
}
|
||||
|
||||
$created = $this->serviceStore->create($tenantId, $userId, $service);
|
||||
return (string) $created->identifier();
|
||||
}
|
||||
|
||||
public function serviceModify(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string
|
||||
{
|
||||
if (!($service instanceof Service)) {
|
||||
throw new \InvalidArgumentException('Service must be an instance of IMAP Service');
|
||||
}
|
||||
|
||||
$updated = $this->serviceStore->modify($tenantId, $userId, $service);
|
||||
return (string) $updated->identifier();
|
||||
}
|
||||
|
||||
public function serviceDestroy(string $tenantId, string $userId, ResourceServiceMutateInterface $service): bool
|
||||
{
|
||||
if (!($service instanceof Service)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->serviceStore->delete($tenantId, $userId, $service->identifier());
|
||||
}
|
||||
|
||||
// ── ProviderServiceDiscoverInterface ──────────────────────────────────────
|
||||
|
||||
public function serviceDiscover(
|
||||
string $tenantId,
|
||||
string $userId,
|
||||
string $identity,
|
||||
?string $location = null,
|
||||
?string $secret = null,
|
||||
): ?ResourceServiceLocationInterface {
|
||||
$discovery = new Discovery();
|
||||
// TODO: Make SSL verification configurable per-tenant
|
||||
$verifySSL = true;
|
||||
return $discovery->discover($identity, $location, $secret, $verifySSL);
|
||||
}
|
||||
|
||||
// ── ProviderServiceTestInterface ──────────────────────────────────────────
|
||||
|
||||
public function serviceTest(ServiceBaseInterface $service, array $options = []): array
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
if (!($service instanceof Service)) {
|
||||
throw new \InvalidArgumentException('Service must be an instance of IMAP Service');
|
||||
}
|
||||
|
||||
// Attempt to authenticate and list mailboxes as a connectivity check
|
||||
$wrapper = RemoteService::freshClient($service);
|
||||
$mailboxes = $wrapper->mailboxes();
|
||||
|
||||
$latency = (int) round((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'IMAP connection successful'
|
||||
. ' (Mailboxes: ' . count($mailboxes) . ')'
|
||||
. ' (Latency: ' . $latency . ' ms)',
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$latency = (int) round((microtime(true) - $startTime) * 1000);
|
||||
|
||||
$location = ($service instanceof Service) ? $service->getLocation() : null;
|
||||
$target = $location
|
||||
? $location->getEncryption() . '://' . $location->getHost() . ':' . $location->getPort()
|
||||
: 'unknown host';
|
||||
|
||||
// stream_socket_client errors are suppressed with @ in gricob — recover them
|
||||
$phpError = error_get_last();
|
||||
$detail = $e->getMessage() !== '' ? $e->getMessage() : ($phpError['message'] ?? '');
|
||||
|
||||
if ($detail === '' && $location !== null) {
|
||||
$host = $location->getHost();
|
||||
if ($host !== '' && gethostbyname($host) === $host) {
|
||||
$detail = "hostname '{$host}' could not be resolved";
|
||||
} else {
|
||||
$detail = 'connection refused or timed out — check port and encryption settings';
|
||||
}
|
||||
} elseif ($detail === '') {
|
||||
$detail = 'no details — check host, port, and encryption settings';
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => sprintf(
|
||||
'Connection to %s failed (%s): %s',
|
||||
$target,
|
||||
(new \ReflectionClass($e))->getShortName(),
|
||||
$detail,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
515
lib/Providers/Service.php
Normal file
515
lib/Providers/Service.php
Normal file
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use Generator;
|
||||
use KTXF\Mail\Collection\CollectionBaseInterface;
|
||||
use KTXF\Mail\Collection\CollectionMutableInterface;
|
||||
use KTXF\Mail\Object\Address;
|
||||
use KTXF\Mail\Object\AddressInterface;
|
||||
use KTXF\Mail\Service\ServiceBaseInterface;
|
||||
use KTXF\Mail\Service\ServiceCollectionMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceConfigurableInterface;
|
||||
use KTXF\Mail\Service\ServiceMutableInterface;
|
||||
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||
use KTXF\Resource\Delta\Delta;
|
||||
use KTXF\Resource\Filter\Filter;
|
||||
use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Resource\Range\IRange;
|
||||
use KTXF\Resource\Range\Range;
|
||||
use KTXF\Resource\Range\RangeTally;
|
||||
use KTXF\Resource\Range\RangeType;
|
||||
use KTXF\Resource\Sort\ISort;
|
||||
use KTXF\Resource\Sort\Sort;
|
||||
use KTXM\ProviderImapMail\Providers\ServiceIdentityBasic;
|
||||
use KTXM\ProviderImapMail\Providers\ServiceLocation;
|
||||
use KTXM\ProviderImapMail\Service\Remote\RemoteMailService;
|
||||
use KTXM\ProviderImapMail\Service\Remote\RemoteService;
|
||||
|
||||
/**
|
||||
* IMAP Mail Service
|
||||
*
|
||||
* Represents a single IMAP account configuration and acts as the primary
|
||||
* entry-point for all mail operations (collections + entities).
|
||||
*
|
||||
* The RemoteMailService is initialised lazily on first use so that the object
|
||||
* can be constructed cheaply for serialisation/deserialisation tasks.
|
||||
*/
|
||||
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface
|
||||
{
|
||||
public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE;
|
||||
|
||||
private const PROVIDER_IDENTIFIER = 'imap';
|
||||
|
||||
private ?string $serviceTenantId = null;
|
||||
private ?string $serviceUserId = null;
|
||||
private ?string $serviceIdentifier = null;
|
||||
private ?string $serviceLabel = null;
|
||||
private bool $serviceEnabled = false;
|
||||
private string $primaryAddress = '';
|
||||
private array $secondaryAddresses = [];
|
||||
private ?ServiceLocation $location = null;
|
||||
private ?ServiceIdentityBasic $identity = null;
|
||||
private array $auxiliary = [];
|
||||
|
||||
private array $serviceAbilities = [
|
||||
self::CAPABILITY_COLLECTION_LIST => true,
|
||||
self::CAPABILITY_COLLECTION_LIST_FILTER => [],
|
||||
self::CAPABILITY_COLLECTION_LIST_SORT => [],
|
||||
self::CAPABILITY_COLLECTION_EXTANT => true,
|
||||
self::CAPABILITY_COLLECTION_FETCH => true,
|
||||
self::CAPABILITY_COLLECTION_CREATE => true,
|
||||
self::CAPABILITY_COLLECTION_UPDATE => true,
|
||||
self::CAPABILITY_COLLECTION_DELETE => true,
|
||||
self::CAPABILITY_ENTITY_LIST => true,
|
||||
self::CAPABILITY_ENTITY_LIST_FILTER => [
|
||||
'seen' => 'b:0:1:1',
|
||||
'flagged' => 'b:0:1:1',
|
||||
self::CAPABILITY_ENTITY_FILTER_FROM => 's:100:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_TO => 's:100:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_SUBJECT => 's:200:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_BODY => 's:200:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_DATE_BEFORE => 's:32:1:1',
|
||||
self::CAPABILITY_ENTITY_FILTER_DATE_AFTER => 's:32:1:1',
|
||||
self::CAPABILITY_ENTITY_FILTER_SIZE_MIN => 'i:0:16:16',
|
||||
self::CAPABILITY_ENTITY_FILTER_SIZE_MAX => 'i:0:32:32',
|
||||
],
|
||||
self::CAPABILITY_ENTITY_LIST_SORT => [],
|
||||
self::CAPABILITY_ENTITY_LIST_RANGE => ['tally' => ['absolute', 'relative']],
|
||||
self::CAPABILITY_ENTITY_EXTANT => true,
|
||||
self::CAPABILITY_ENTITY_FETCH => true,
|
||||
];
|
||||
|
||||
private RemoteMailService $mailService;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
// ── Lazy initialisation ───────────────────────────────────────────────────
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
if (!isset($this->mailService)) {
|
||||
$wrapper = RemoteService::freshClient($this);
|
||||
$this->mailService = RemoteService::mailService($this, $wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Store (MongoDB persistence) ───────────────────────────────────────────
|
||||
|
||||
public function toStore(): array
|
||||
{
|
||||
return array_filter([
|
||||
'tid' => $this->serviceTenantId,
|
||||
'uid' => $this->serviceUserId,
|
||||
'sid' => $this->serviceIdentifier,
|
||||
'label' => $this->serviceLabel,
|
||||
'enabled' => $this->serviceEnabled,
|
||||
'primaryAddress' => $this->primaryAddress,
|
||||
'secondaryAddresses'=> $this->secondaryAddresses,
|
||||
'location' => $this->location?->toStore(),
|
||||
'identity' => $this->identity?->toStore(),
|
||||
'auxiliary' => $this->auxiliary,
|
||||
], fn($v) => $v !== null);
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
{
|
||||
$this->serviceTenantId = $data['tid'] ?? null;
|
||||
$this->serviceUserId = $data['uid'] ?? null;
|
||||
$this->serviceIdentifier = $data['sid'] ?? null;
|
||||
$this->serviceLabel = $data['label'] ?? '';
|
||||
$this->serviceEnabled = $data['enabled'] ?? false;
|
||||
|
||||
if (isset($data['primaryAddress'])) {
|
||||
$this->primaryAddress = $data['primaryAddress'];
|
||||
}
|
||||
if (isset($data['secondaryAddresses']) && is_array($data['secondaryAddresses'])) {
|
||||
$this->secondaryAddresses = $data['secondaryAddresses'];
|
||||
}
|
||||
if (isset($data['location'])) {
|
||||
$this->location = (new ServiceLocation())->fromStore($data['location']);
|
||||
}
|
||||
if (isset($data['identity'])) {
|
||||
$this->identity = (new ServiceIdentityBasic())->fromStore($data['identity']);
|
||||
}
|
||||
if (isset($data['auxiliary']) && is_array($data['auxiliary'])) {
|
||||
$this->auxiliary = $data['auxiliary'];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── JSON ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_filter([
|
||||
self::JSON_PROPERTY_TYPE => self::JSON_TYPE,
|
||||
self::JSON_PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER,
|
||||
self::JSON_PROPERTY_IDENTIFIER => $this->serviceIdentifier,
|
||||
self::JSON_PROPERTY_LABEL => $this->serviceLabel,
|
||||
self::JSON_PROPERTY_ENABLED => $this->serviceEnabled,
|
||||
self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities,
|
||||
self::JSON_PROPERTY_PRIMARY_ADDRESS => $this->primaryAddress,
|
||||
self::JSON_PROPERTY_SECONDARY_ADDRESSES => $this->secondaryAddresses,
|
||||
self::JSON_PROPERTY_LOCATION => $this->location?->jsonSerialize(),
|
||||
self::JSON_PROPERTY_IDENTITY => $this->identity?->jsonSerialize(),
|
||||
self::JSON_PROPERTY_AUXILIARY => $this->auxiliary,
|
||||
], fn($v) => $v !== null);
|
||||
}
|
||||
|
||||
public function jsonDeserialize(array|string $data): static
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
if (isset($data[self::JSON_PROPERTY_LABEL])) {
|
||||
$this->setLabel($data[self::JSON_PROPERTY_LABEL]);
|
||||
}
|
||||
if (isset($data[self::JSON_PROPERTY_ENABLED])) {
|
||||
$this->setEnabled($data[self::JSON_PROPERTY_ENABLED]);
|
||||
}
|
||||
if (isset($data[self::JSON_PROPERTY_LOCATION])) {
|
||||
$this->setLocation($this->freshLocation(null, $data[self::JSON_PROPERTY_LOCATION]));
|
||||
}
|
||||
if (isset($data[self::JSON_PROPERTY_IDENTITY])) {
|
||||
$this->setIdentity($this->freshIdentity(null, $data[self::JSON_PROPERTY_IDENTITY]));
|
||||
}
|
||||
if (isset($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]) && is_string($data[self::JSON_PROPERTY_PRIMARY_ADDRESS])) {
|
||||
$this->setPrimaryAddress(new Address($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]));
|
||||
}
|
||||
if (isset($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES])) {
|
||||
$this->setSecondaryAddresses(array_map(
|
||||
fn($addr) => new Address(is_array($addr) ? ($addr['address'] ?? $addr) : $addr),
|
||||
$data[self::JSON_PROPERTY_SECONDARY_ADDRESSES]
|
||||
));
|
||||
}
|
||||
if (isset($data[self::JSON_PROPERTY_AUXILIARY]) && is_array($data[self::JSON_PROPERTY_AUXILIARY])) {
|
||||
$this->setAuxiliary($data[self::JSON_PROPERTY_AUXILIARY]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── ServiceBaseInterface ──────────────────────────────────────────────────
|
||||
|
||||
public function capable(string $value): bool
|
||||
{
|
||||
return isset($this->serviceAbilities[$value]);
|
||||
}
|
||||
|
||||
public function capabilities(): array
|
||||
{
|
||||
return $this->serviceAbilities;
|
||||
}
|
||||
|
||||
public function provider(): string
|
||||
{
|
||||
return self::PROVIDER_IDENTIFIER;
|
||||
}
|
||||
|
||||
public function identifier(): string|int
|
||||
{
|
||||
return $this->serviceIdentifier;
|
||||
}
|
||||
|
||||
// ── ServiceMutableInterface ───────────────────────────────────────────────
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->serviceLabel;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): static
|
||||
{
|
||||
$this->serviceLabel = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnabled(): bool
|
||||
{
|
||||
return $this->serviceEnabled;
|
||||
}
|
||||
|
||||
public function setEnabled(bool $enabled): static
|
||||
{
|
||||
$this->serviceEnabled = $enabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrimaryAddress(): AddressInterface
|
||||
{
|
||||
return new Address($this->primaryAddress);
|
||||
}
|
||||
|
||||
public function setPrimaryAddress(AddressInterface $value): static
|
||||
{
|
||||
$this->primaryAddress = $value->getAddress();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSecondaryAddresses(): array
|
||||
{
|
||||
return $this->secondaryAddresses;
|
||||
}
|
||||
|
||||
public function setSecondaryAddresses(array $addresses): static
|
||||
{
|
||||
$this->secondaryAddresses = $addresses;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasAddress(string $address): bool
|
||||
{
|
||||
$address = strtolower(trim($address));
|
||||
|
||||
if ($this->primaryAddress && strtolower($this->primaryAddress) === $address) {
|
||||
return true;
|
||||
}
|
||||
foreach ($this->secondaryAddresses as $secondary) {
|
||||
$secondaryAddr = $secondary instanceof AddressInterface ? $secondary->getAddress() : (string) $secondary;
|
||||
if (strtolower($secondaryAddr) === $address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── ServiceConfigurableInterface ──────────────────────────────────────────
|
||||
|
||||
public function getLocation(): ServiceLocation
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function setLocation(ResourceServiceLocationInterface $location): static
|
||||
{
|
||||
$this->location = $location;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function freshLocation(?string $type = null, array $data = []): ServiceLocation
|
||||
{
|
||||
$loc = new ServiceLocation();
|
||||
$loc->jsonDeserialize($data);
|
||||
return $loc;
|
||||
}
|
||||
|
||||
public function getIdentity(): ServiceIdentityBasic
|
||||
{
|
||||
return $this->identity;
|
||||
}
|
||||
|
||||
public function setIdentity(ResourceServiceIdentityInterface $identity): static
|
||||
{
|
||||
$this->identity = $identity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function freshIdentity(?string $type = null, array $data = []): ServiceIdentityBasic
|
||||
{
|
||||
$id = new ServiceIdentityBasic();
|
||||
$id->jsonDeserialize($data);
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function getDebug(): bool
|
||||
{
|
||||
return ($this->auxiliary['debug'] ?? false) === true;
|
||||
}
|
||||
|
||||
public function setDebug(bool $debug): static
|
||||
{
|
||||
$this->auxiliary['debug'] = $debug;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuxiliary(): array
|
||||
{
|
||||
return $this->auxiliary;
|
||||
}
|
||||
|
||||
public function setAuxiliary(array $auxiliary): static
|
||||
{
|
||||
$this->auxiliary = $auxiliary;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── Collection operations ─────────────────────────────────────────────────
|
||||
|
||||
public function collectionList(string|int|null $location, ?\KTXF\Resource\Filter\IFilter $filter = null, ?\KTXF\Resource\Sort\ISort $sort = null): array
|
||||
{
|
||||
$this->initialize();
|
||||
return $this->mailService->collectionList();
|
||||
}
|
||||
|
||||
public function collectionListFilter(): Filter
|
||||
{
|
||||
return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []);
|
||||
}
|
||||
|
||||
public function collectionListSort(): Sort
|
||||
{
|
||||
return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []);
|
||||
}
|
||||
|
||||
public function collectionExtant(string|int ...$identifiers): array
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$existing = $this->mailService->collectionList();
|
||||
$extant = [];
|
||||
foreach ($identifiers as $id) {
|
||||
$extant[(string) $id] = isset($existing[(string) $id]);
|
||||
}
|
||||
return $extant;
|
||||
}
|
||||
|
||||
public function collectionFetch(string|int $identifier): ?CollectionBaseInterface
|
||||
{
|
||||
$this->initialize();
|
||||
return $this->mailService->collectionFetch((string) $identifier);
|
||||
}
|
||||
|
||||
public function collectionFresh(): CollectionMutableInterface
|
||||
{
|
||||
return new CollectionResource();
|
||||
}
|
||||
|
||||
public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionBaseInterface
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
// Resolve the full name: if a parent location is given, prepend it
|
||||
$label = $collection->getProperties()->getLabel() ?? '';
|
||||
if ($location !== null && $location !== '') {
|
||||
// Determine the hierarchy delimiter from an existing mailbox, default to '/'
|
||||
$existing = $this->mailService->collectionList();
|
||||
$delimiter = '/';
|
||||
foreach ($existing as $c) {
|
||||
$props = $c->getProperties();
|
||||
if ($props instanceof CollectionProperties) {
|
||||
$d = $props->getDelimiter();
|
||||
if ($d !== null && $d !== '') {
|
||||
$delimiter = $d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$label = rtrim((string) $location, $delimiter) . $delimiter . ltrim($label, $delimiter);
|
||||
}
|
||||
|
||||
return $this->mailService->collectionCreate($label);
|
||||
}
|
||||
|
||||
public function collectionUpdate(string|int $identifier, CollectionMutableInterface $collection): CollectionBaseInterface
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
// In IMAP, "update" = rename to the new label
|
||||
$newName = $collection->getProperties()->getLabel() ?? (string) $identifier;
|
||||
return $this->mailService->collectionRename((string) $identifier, $newName);
|
||||
}
|
||||
|
||||
public function collectionDelete(string|int $identifier, bool $force = false, bool $recursive = false): bool
|
||||
{
|
||||
$this->initialize();
|
||||
return $this->mailService->collectionDestroy((string) $identifier);
|
||||
}
|
||||
|
||||
public function collectionMove(string|int $identifier, string|int|null $targetLocation): CollectionBaseInterface
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
// IMAP RENAME effectively moves+renames the mailbox
|
||||
$existing = $this->mailService->collectionFetch((string) $identifier);
|
||||
$label = $existing?->getProperties()->getLabel() ?? basename((string) $identifier);
|
||||
$newName = $targetLocation !== null ? rtrim((string) $targetLocation, '/') . '/' . $label : $label;
|
||||
|
||||
return $this->mailService->collectionRename((string) $identifier, $newName);
|
||||
}
|
||||
|
||||
// ── Entity operations ─────────────────────────────────────────────────────
|
||||
|
||||
public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$uids = $this->mailService->entityList((string) $collection, $filter, $range);
|
||||
if (empty($uids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->mailService->entityFetch((string) $collection, ...$uids);
|
||||
}
|
||||
|
||||
public function entityListStream(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$uids = $this->mailService->entityList((string) $collection, $filter, $range);
|
||||
if (empty($uids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield from $this->mailService->entityFetchStream((string) $collection, ...$uids);
|
||||
}
|
||||
|
||||
public function entityListFilter(): Filter
|
||||
{
|
||||
return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []);
|
||||
}
|
||||
|
||||
public function entityListSort(): Sort
|
||||
{
|
||||
return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []);
|
||||
}
|
||||
|
||||
public function entityListRange(RangeType $type): IRange
|
||||
{
|
||||
return match ($type) {
|
||||
RangeType::TALLY => new RangeTally(),
|
||||
default => new Range(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delta sync is not supported for IMAP (no CONDSTORE/QRESYNC initially).
|
||||
* Returns an empty Delta so callers detect the absence of changes gracefully.
|
||||
*/
|
||||
public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): Delta
|
||||
{
|
||||
return new Delta(signature: $signature);
|
||||
}
|
||||
|
||||
public function entityExtant(string|int $collection, string|int ...$identifiers): array
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$allUids = $this->mailService->entityList((string) $collection);
|
||||
$uidSet = array_flip($allUids); // int[] → [uid => index]
|
||||
$extant = [];
|
||||
foreach ($identifiers as $id) {
|
||||
$extant[$id] = isset($uidSet[(int) $id]);
|
||||
}
|
||||
return $extant;
|
||||
}
|
||||
|
||||
public function entityFetch(string|int $collection, string|int ...$identifiers): array
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$uids = array_map('intval', $identifiers);
|
||||
return $this->mailService->entityFetch((string) $collection, ...$uids);
|
||||
}
|
||||
}
|
||||
74
lib/Providers/ServiceIdentityBasic.php
Normal file
74
lib/Providers/ServiceIdentityBasic.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use KTXF\Resource\Provider\ResourceServiceIdentityBasic;
|
||||
|
||||
/**
|
||||
* IMAP Service Basic Identity
|
||||
*
|
||||
* Username / password (or token) authentication.
|
||||
*/
|
||||
class ServiceIdentityBasic implements ResourceServiceIdentityBasic
|
||||
{
|
||||
public function __construct(
|
||||
private string $identity = '',
|
||||
private string $secret = '',
|
||||
) {}
|
||||
|
||||
public function toStore(): array
|
||||
{
|
||||
return [
|
||||
'type' => self::TYPE_BASIC,
|
||||
'identity' => $this->identity,
|
||||
'secret' => $this->secret,
|
||||
];
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
{
|
||||
return new static(
|
||||
identity: $data['identity'] ?? '',
|
||||
secret: $data['secret'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => self::TYPE_BASIC,
|
||||
'identity' => $this->identity,
|
||||
// Secret intentionally omitted from serialisation
|
||||
];
|
||||
}
|
||||
|
||||
public function jsonDeserialize(array|string $data): static
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
}
|
||||
|
||||
$this->identity = $data['identity'] ?? '';
|
||||
$this->secret = $data['secret'] ?? '';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return self::TYPE_BASIC;
|
||||
}
|
||||
|
||||
public function getIdentity(): string { return $this->identity; }
|
||||
public function setIdentity(string $v): void { $this->identity = $v; }
|
||||
|
||||
public function getSecret(): string { return $this->secret; }
|
||||
public function setSecret(string $v): void { $this->secret = $v; }
|
||||
}
|
||||
134
lib/Providers/ServiceLocation.php
Normal file
134
lib/Providers/ServiceLocation.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
use Gricob\IMAP\Configuration;
|
||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||
|
||||
/**
|
||||
* IMAP Service Location
|
||||
*
|
||||
* Connection details for an IMAP server (host / port / encryption).
|
||||
*/
|
||||
class ServiceLocation implements ResourceServiceLocationInterface
|
||||
{
|
||||
public function __construct(
|
||||
private string $host = '',
|
||||
private int $port = 993,
|
||||
private string $encryption = 'ssl', // ssl | tls | starttls | none
|
||||
private bool $verifyPeer = true,
|
||||
private bool $verifyPeerName = true,
|
||||
private bool $allowSelfSigned = false,
|
||||
) {}
|
||||
|
||||
// ── Serialisation ────────────────────────────────────────────────────────
|
||||
|
||||
public function toStore(): array
|
||||
{
|
||||
return $this->jsonSerialize();
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
{
|
||||
return $this->jsonDeserialize($data);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_filter([
|
||||
'type' => self::TYPE_URI,
|
||||
'host' => $this->host,
|
||||
'port' => $this->port,
|
||||
'encryption' => $this->encryption,
|
||||
'verifyPeer' => $this->verifyPeer,
|
||||
'verifyPeerName' => $this->verifyPeerName,
|
||||
'allowSelfSigned' => $this->allowSelfSigned,
|
||||
], fn($v) => $v !== null && $v !== '');
|
||||
}
|
||||
|
||||
public function jsonDeserialize(array|string $data): static
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
}
|
||||
|
||||
$this->host = $data['host'] ?? '';
|
||||
$this->port = (int)($data['port'] ?? 993);
|
||||
$this->encryption = $data['encryption'] ?? 'ssl';
|
||||
$this->verifyPeer = $data['verifyPeer'] ?? true;
|
||||
$this->verifyPeerName = $data['verifyPeerName'] ?? true;
|
||||
$this->allowSelfSigned = $data['allowSelfSigned'] ?? false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── ResourceServiceLocationInterface ─────────────────────────────────────
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return self::TYPE_URI;
|
||||
}
|
||||
|
||||
public function location(): string
|
||||
{
|
||||
return $this->encryption . '://' . $this->host . ':' . $this->port;
|
||||
}
|
||||
|
||||
// ── Accessors ────────────────────────────────────────────────────────────
|
||||
|
||||
public function getHost(): string { return $this->host; }
|
||||
public function setHost(string $v): void { $this->host = $v; }
|
||||
|
||||
public function getPort(): int { return $this->port; }
|
||||
public function setPort(int $v): void { $this->port = $v; }
|
||||
|
||||
public function getEncryption(): string { return $this->encryption; }
|
||||
public function setEncryption(string $v): void { $this->encryption = $v; }
|
||||
|
||||
public function getVerifyPeer(): bool { return $this->verifyPeer; }
|
||||
public function setVerifyPeer(bool $v): void { $this->verifyPeer = $v; }
|
||||
|
||||
public function getVerifyPeerName(): bool { return $this->verifyPeerName; }
|
||||
public function setVerifyPeerName(bool $v): void { $this->verifyPeerName = $v; }
|
||||
|
||||
public function getAllowSelfSigned(): bool { return $this->allowSelfSigned; }
|
||||
public function setAllowSelfSigned(bool $v): void { $this->allowSelfSigned = $v; }
|
||||
|
||||
// ── gricob helper ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build a Gricob IMAP Configuration from this location.
|
||||
*
|
||||
* gricob passes the transport directly to stream_socket_client:
|
||||
* 'ssl' → ssl://host:port (implicit TLS, port 993)
|
||||
* 'tcp' → tcp://host:port (plain TCP; STARTTLS negotiation is not
|
||||
* supported by gricob, so starttls/none both
|
||||
* use plain TCP)
|
||||
*/
|
||||
public function toConfiguration(): Configuration
|
||||
{
|
||||
// Map our encryption label to a stream_socket_client transport.
|
||||
// gricob has no STARTTLS negotiation, so starttls falls back to tcp.
|
||||
$transport = match ($this->encryption) {
|
||||
'ssl', 'tls' => 'ssl',
|
||||
default => 'tcp', // starttls, none
|
||||
};
|
||||
|
||||
return new Configuration(
|
||||
transport: $transport,
|
||||
host: $this->host,
|
||||
port: $this->port,
|
||||
verifyPeer: $this->verifyPeer,
|
||||
verifyPeerName: $this->verifyPeerName,
|
||||
allowSelfSigned: $this->allowSelfSigned,
|
||||
useUid: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
83
lib/Providers/ServiceSettings.php
Normal file
83
lib/Providers/ServiceSettings.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
enum ServiceMode: string
|
||||
{
|
||||
case Live = 'live';
|
||||
case Cached = 'cached';
|
||||
}
|
||||
|
||||
enum CacheSyncStrategy: string
|
||||
{
|
||||
case Manual = 'manual';
|
||||
case Interval = 'interval';
|
||||
case Push = 'push';
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-service operational settings for the IMAP provider.
|
||||
*
|
||||
* - debug: Attach a PSR-3 logger to the IMAP client when true
|
||||
* - mode: live (all reads/writes hit IMAP) | cached (reads from MongoDB)
|
||||
* - cacheSync: How the cache is kept fresh (only relevant in cached mode)
|
||||
*/
|
||||
class ServiceSettings
|
||||
{
|
||||
public function __construct(
|
||||
private bool $debug = false,
|
||||
private ServiceMode $mode = ServiceMode::Live,
|
||||
private CacheSyncStrategy $cacheSync = CacheSyncStrategy::Interval,
|
||||
) {}
|
||||
|
||||
// ── Serialisation ────────────────────────────────────────────────────────
|
||||
|
||||
public function toStore(): array
|
||||
{
|
||||
return [
|
||||
'debug' => $this->debug,
|
||||
'mode' => $this->mode->value,
|
||||
'cacheSync' => $this->cacheSync->value,
|
||||
];
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
{
|
||||
$this->debug = $data['debug'] ?? false;
|
||||
$this->mode = ServiceMode::from($data['mode'] ?? ServiceMode::Live->value);
|
||||
$this->cacheSync = CacheSyncStrategy::from($data['cacheSync'] ?? CacheSyncStrategy::Interval->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toStore();
|
||||
}
|
||||
|
||||
public function jsonDeserialize(array|string $data): static
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
}
|
||||
return $this->fromStore($data);
|
||||
}
|
||||
|
||||
// ── Accessors ────────────────────────────────────────────────────────────
|
||||
|
||||
public function getDebug(): bool { return $this->debug; }
|
||||
public function setDebug(bool $v): static { $this->debug = $v; return $this; }
|
||||
|
||||
public function getMode(): ServiceMode { return $this->mode; }
|
||||
public function setMode(ServiceMode $v): static { $this->mode = $v; return $this; }
|
||||
|
||||
public function getCacheSync(): CacheSyncStrategy { return $this->cacheSync; }
|
||||
public function setCacheSync(CacheSyncStrategy $v): static { $this->cacheSync = $v; return $this; }
|
||||
}
|
||||
Reference in New Issue
Block a user