Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.33% covered (success)
97.33%
73 / 75
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MailFolderDto
97.30% covered (success)
97.30%
72 / 74
83.33% covered (warning)
83.33%
5 / 6
38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isInbox
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 getDisplayName
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
19
 hasChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 fromArray
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
9.09
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Mail\Domain\Model;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11/**
12 * Mail Folder Data Transfer Object.
13 *
14 * Represents a mailbox folder node, its RFC 6154 special-use role,
15 * unread counters, and hierarchical child subfolders.
16 *
17 * @package App\Modules\Mail\Domain\Model
18 */
19final readonly class MailFolderDto
20{
21    /**
22     * MailFolderDto constructor.
23     *
24     * @param string                  $id             Unique folder identifier / encoded path.
25     * @param string                  $name           Display folder name.
26     * @param string                  $path           Full protocol mailbox path (e.g. INBOX/Work).
27     * @param string                  $delimiter      Hierarchy delimiter (e.g. '/' or '.').
28     * @param array<string>           $attributes     IMAP folder attributes (\HasChildren, \Subscribed).
29     * @param string|null             $specialUse     Special role: inbox, sent, drafts, trash, spam, archive.
30     * @param int                     $totalMessages  Total message count in folder.
31     * @param int                     $unseenMessages Unseen / unread message count in folder.
32     * @param int|null                $uidNext        Expected next UID.
33     * @param int|null                $uidValidity    Mailbox validity identifier.
34     * @param int|null                $highestModseq  Highest modification sequence (CONDSTORE).
35     * @param array<self>             $children       Subfolders list.
36     */
37    public function __construct(
38        public string $id,
39        public string $name,
40        public string $path,
41        public string $delimiter = '/',
42        public array $attributes = [],
43        public ?string $specialUse = null,
44        public int $totalMessages = 0,
45        public int $unseenMessages = 0,
46        public ?int $uidNext = null,
47        public ?int $uidValidity = null,
48        public ?int $highestModseq = null,
49        public array $children = []
50    ) {
51    }
52
53    /**
54     * Returns whether this folder is an inbox folder.
55     *
56     * @return bool True if inbox.
57     */
58    public function isInbox(): bool
59    {
60        $delim = $this->delimiter !== '' ? $this->delimiter : '/';
61        $pattern = '/^inbox[' . preg_quote($delim, '/') . ']/i';
62        $relative = preg_replace($pattern, '', $this->path) ?? $this->path;
63        if (str_contains($relative, $delim) || ($relative !== '' && strtoupper($this->path) !== 'INBOX')) {
64            return false;
65        }
66
67        return $this->specialUse === 'inbox' || strtoupper($this->path) === 'INBOX';
68    }
69
70    /**
71     * Returns the localized display name of the folder for UI.
72     *
73     * @return string Human-friendly folder name in Polish.
74     */
75    public function getDisplayName(): string
76    {
77        $delim = $this->delimiter !== '' ? $this->delimiter : '/';
78        $pattern = '/^inbox[' . preg_quote($delim, '/') . ']/i';
79        $relative = preg_replace($pattern, '', $this->path) ?? $this->path;
80        if (str_contains($relative, $delim)) {
81            return $this->name;
82        }
83
84        $role = $this->specialUse !== null ? strtolower($this->specialUse) : null;
85        if ($role === null) {
86            $upperName = strtoupper($this->name);
87            if (strtoupper($this->path) === 'INBOX' || $upperName === 'INBOX') {
88                $role = 'inbox';
89            } elseif (in_array($upperName, ['SENT', 'SENT MESSAGES', 'SENT ITEMS'], true)) {
90                $role = 'sent';
91            } elseif (in_array($upperName, ['DRAFTS'], true)) {
92                $role = 'drafts';
93            } elseif (in_array($upperName, ['TRASH', 'DELETED', 'DELETED ITEMS', 'BIN'], true)) {
94                $role = 'trash';
95            } elseif (in_array($upperName, ['SPAM', 'JUNK'], true)) {
96                $role = 'spam';
97            } elseif (in_array($upperName, ['ARCHIVE', 'ARCHIVES'], true)) {
98                $role = 'archive';
99            }
100        }
101
102        return match ($role) {
103            'inbox' => 'Inbox',
104            'sent' => 'Sent',
105            'drafts' => 'Drafts',
106            'trash' => 'Trash',
107            'spam', 'junk' => 'Spam',
108            'archive' => 'Archive',
109            default => $this->name,
110        };
111    }
112
113    /**
114     * Returns whether this folder has subfolders.
115     *
116     * @return bool True if children exist.
117     */
118    public function hasChildren(): bool
119    {
120        return $this->children !== [];
121    }
122
123    /**
124     * Converts DTO to array for JSON caching.
125     *
126     * @return array<string, mixed> Serialized data.
127     */
128    public function toArray(): array
129    {
130        $childrenArray = [];
131        foreach ($this->children as $child) {
132            $childrenArray[] = $child->toArray();
133        }
134
135        return [
136            'id'             => $this->id,
137            'name'           => $this->name,
138            'displayName'    => $this->getDisplayName(),
139            'path'           => $this->path,
140            'delimiter'      => $this->delimiter,
141            'attributes'     => $this->attributes,
142            'specialUse'     => $this->specialUse,
143            'totalMessages'  => $this->totalMessages,
144            'unseenMessages' => $this->unseenMessages,
145            'uidNext'        => $this->uidNext,
146            'uidValidity'    => $this->uidValidity,
147            'highestModseq'  => $this->highestModseq,
148            'children'       => $childrenArray,
149        ];
150    }
151
152    /**
153     * Reconstructs DTO instance from cached array data.
154     *
155     * @param array<string, mixed> $data Cached data.
156     * @return self Reconstructed DTO.
157     */
158    public static function fromArray(array $data): self
159    {
160        $children = [];
161        if (isset($data['children']) && is_array($data['children'])) {
162            foreach ($data['children'] as $childData) {
163                if (is_array($childData)) {
164                    $children[] = self::fromArray($childData);
165                }
166            }
167        }
168
169        return new self(
170            id: (string) ($data['id'] ?? ''),
171            name: (string) ($data['name'] ?? ''),
172            path: (string) ($data['path'] ?? ''),
173            delimiter: (string) ($data['delimiter'] ?? '/'),
174            attributes: (array) ($data['attributes'] ?? []),
175            specialUse: isset($data['specialUse']) ? (string) $data['specialUse'] : null,
176            totalMessages: (int) ($data['totalMessages'] ?? 0),
177            unseenMessages: (int) ($data['unseenMessages'] ?? 0),
178            uidNext: isset($data['uidNext']) ? (int) $data['uidNext'] : null,
179            uidValidity: isset($data['uidValidity']) ? (int) $data['uidValidity'] : null,
180            highestModseq: isset($data['highestModseq']) ? (int) $data['highestModseq'] : null,
181            children: $children
182        );
183    }
184}