Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
63 / 63
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
ClientMailbox
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
8 / 8
23
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stringOrNull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 decodeJsonArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 fromRow
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
9
 isDefaultForUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copyWith
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 withIsDefault
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withDecryptedPassword
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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 * Client User and Business Mailbox Aggregate Model.
13 *
14 * Encapsulates incoming protocol (IMAP/JMAP), dedicated outgoing SMTP transport,
15 * special folder mappings, and synchronization sequence metadata.
16 *
17 * @package App\Modules\Mail\Domain\Model
18 */
19final readonly class ClientMailbox
20{
21    /**
22     * ClientMailbox constructor.
23     *
24     * @param int         $id                Primary ID.
25     * @param string      $name              Mailbox label / account name.
26     * @param string      $email             Email address.
27     * @param string      $fromName          Display sender name.
28     * @param int         $mailServerId      Associated MailServer ID.
29     * @param string      $protocolType      Protocol type ('imap', 'jmap', 'exchange').
30     * @param string      $username          Login username.
31     * @param string      $password          AEAD encrypted password.
32     * @param string|null $smtpHost          Dedicated SMTP hostname or null for server fallback.
33     * @param int|null    $smtpPort          Dedicated SMTP port or null for server fallback.
34     * @param string      $smtpEncryption    Dedicated SMTP encryption ('ssl', 'tls', 'starttls', 'none').
35     * @param string      $smtpAuthMethod    Dedicated SMTP auth method ('login', 'plain', 'cram-md5').
36     * @param string|null $smtpUsername      Dedicated SMTP username or null for mailbox fallback.
37     * @param string|null $smtpPassword      Dedicated AEAD encrypted SMTP password or null for fallback.
38     * @param string|null $smtpFromEmail     Dedicated SMTP sender email or null for mailbox fallback.
39     * @param string|null $smtpFromName      Dedicated SMTP sender name or null for mailbox fallback.
40     * @param string|null $smtpReplyToEmail  Dedicated reply-to email.
41     * @param string      $folderInbox       Mapped Inbox folder name.
42     * @param string|null $folderSent        Mapped Sent folder name.
43     * @param string|null $folderDrafts      Mapped Drafts folder name.
44     * @param string|null $folderTrash       Mapped Trash folder name.
45     * @param string|null $folderSpam        Mapped Spam folder name.
46     * @param string|null $folderArchive     Mapped Archive folder name.
47     * @param string|null $signatureHtml     Configured HTML signature or null.
48     * @param array<string, mixed>|null $folderOrder Custom folder ordering.
49     * @param array<string, mixed>|null $cachedFolders Cached folder tree hierarchy.
50     * @param int|null    $highestModseq     IMAP CONDSTORE highest modification sequence.
51     * @param int|null    $uidNext           IMAP UIDNEXT expected next message UID.
52     * @param int|null    $uidValidity       IMAP UIDVALIDITY mailbox identifier.
53     * @param bool        $isShared          Whether shared among team members.
54     * @param bool        $isDefault         Whether this is the default/primary mailbox.
55     * @param bool        $showBadge         Whether unread count is included in topbar badge.
56     * @param bool        $notifyNewMail     Whether new mail triggers desktop notifications.
57     * @param int         $sortOrder         Display ordering sequence.
58     * @param string      $syncStatus        Status: ok, auth_error, connection_error, pending.
59     * @param string|null $lastSyncAt        Last successful sync timestamp.
60     * @param bool        $isActive          Active flag.
61     * @param int         $owner             Owner user ID.
62     * @param array<int>  $coOwners          List of co-owner user IDs.
63     * @param array<int>  $defaultForUsers   List of user IDs for whom this mailbox is default.
64     */
65    public function __construct(
66        public int $id,
67        public string $name,
68        public string $email,
69        public string $fromName,
70        public int $mailServerId,
71        public string $protocolType = 'imap',
72        public string $username = '',
73        public string $password = '',
74        public ?string $smtpHost = null,
75        public ?int $smtpPort = 465,
76        public string $smtpEncryption = 'ssl',
77        public string $smtpAuthMethod = 'login',
78        public ?string $smtpUsername = null,
79        public ?string $smtpPassword = null,
80        public ?string $smtpFromEmail = null,
81        public ?string $smtpFromName = null,
82        public ?string $smtpReplyToEmail = null,
83        public string $folderInbox = 'INBOX',
84        public ?string $folderSent = null,
85        public ?string $folderDrafts = null,
86        public ?string $folderTrash = null,
87        public ?string $folderSpam = null,
88        public ?string $folderArchive = null,
89        public ?string $signatureHtml = null,
90        public ?array $folderOrder = null,
91        public ?array $cachedFolders = null,
92        public ?int $highestModseq = null,
93        public ?int $uidNext = null,
94        public ?int $uidValidity = null,
95        public bool $isShared = false,
96        public bool $isDefault = false,
97        public bool $showBadge = true,
98        public bool $notifyNewMail = true,
99        public int $sortOrder = 0,
100        public string $syncStatus = 'ok',
101        public ?string $lastSyncAt = null,
102        public bool $isActive = true,
103        public int $owner = 1,
104        public array $coOwners = [],
105        public array $defaultForUsers = []
106    ) {
107    }
108
109    /**
110     * Extracts a nullable string from array if set and not empty.
111     *
112     * @param array<string, mixed> $row Database record.
113     * @param string               $key Field key.
114     * @return string|null Non-empty string or null.
115     */
116    private static function stringOrNull(array $row, string $key): ?string
117    {
118        return isset($row[$key]) && $row[$key] !== '' ? (string) $row[$key] : null;
119    }
120
121    /**
122     * Safely decodes a JSON or array field from database row.
123     *
124     * @param mixed $value Raw field value.
125     * @return array<string, mixed>|null Decoded array or null.
126     */
127    private static function decodeJsonArray(mixed $value): ?array
128    {
129        if (is_string($value) && $value !== '') {
130            $decoded = json_decode($value, true);
131            return is_array($decoded) ? $decoded : null;
132        }
133
134        return is_array($value) ? $value : null;
135    }
136
137    /**
138     * Instantiates ClientMailbox from database row.
139     *
140     * @param array<string, mixed> $row Database record.
141     * @param array<int>           $coOwners Optional co-owner user IDs.
142     * @return self Populated instance.
143     */
144    public static function fromRow(array $row, array $coOwners = []): self
145    {
146        $defaultForUsers = [];
147        if (isset($row['default_for_users'])) {
148            $decoded = self::decodeJsonArray($row['default_for_users']);
149            if (is_array($decoded)) {
150                $defaultForUsers = array_map('intval', array_values($decoded));
151            }
152        }
153
154        return new self(
155            id: (int) ($row['id'] ?? 0),
156            name: (string) ($row['name'] ?? ''),
157            email: (string) ($row['email'] ?? ''),
158            fromName: (string) ($row['from_name'] ?? ''),
159            mailServerId: (int) ($row['mail_server_id'] ?? 0),
160            protocolType: (string) ($row['protocol_type'] ?? 'imap'),
161            username: (string) ($row['username'] ?? ''),
162            password: (string) ($row['password'] ?? ''),
163            smtpHost: self::stringOrNull($row, 'smtp_host'),
164            smtpPort: isset($row['smtp_port']) ? (int) $row['smtp_port'] : null,
165            smtpEncryption: (string) ($row['smtp_encryption'] ?? 'ssl'),
166            smtpAuthMethod: (string) ($row['smtp_auth_method'] ?? 'login'),
167            smtpUsername: self::stringOrNull($row, 'smtp_username'),
168            smtpPassword: self::stringOrNull($row, 'smtp_password'),
169            smtpFromEmail: self::stringOrNull($row, 'smtp_from_email'),
170            smtpFromName: self::stringOrNull($row, 'smtp_from_name'),
171            smtpReplyToEmail: self::stringOrNull($row, 'smtp_reply_to_email'),
172            folderInbox: (string) ($row['folder_inbox'] ?? 'INBOX'),
173            folderSent: self::stringOrNull($row, 'folder_sent'),
174            folderDrafts: self::stringOrNull($row, 'folder_drafts'),
175            folderTrash: self::stringOrNull($row, 'folder_trash'),
176            folderSpam: self::stringOrNull($row, 'folder_spam'),
177            folderArchive: self::stringOrNull($row, 'folder_archive'),
178            signatureHtml: self::stringOrNull($row, 'signature_html'),
179            folderOrder: self::decodeJsonArray($row['folder_order'] ?? null),
180            cachedFolders: self::decodeJsonArray($row['cached_folders'] ?? null),
181            highestModseq: isset($row['highest_modseq']) ? (int) $row['highest_modseq'] : null,
182            uidNext: isset($row['uid_next']) ? (int) $row['uid_next'] : null,
183            uidValidity: isset($row['uid_validity']) ? (int) $row['uid_validity'] : null,
184            isShared: (bool) ($row['is_shared'] ?? false),
185            isDefault: (bool) ($row['is_default'] ?? false),
186            showBadge: (bool) ($row['show_badge'] ?? true),
187            notifyNewMail: (bool) ($row['notify_new_mail'] ?? true),
188            sortOrder: (int) ($row['sort_order'] ?? 0),
189            syncStatus: (string) ($row['sync_status'] ?? 'ok'),
190            lastSyncAt: self::stringOrNull($row, 'last_sync_at'),
191            isActive: isset($row['is_active'])
192                ? (bool) ($row['is_active'])
193                : (($row['status'] ?? 'active') === 'active' && ((int) ($row['special_access'] ?? 1) === 1)),
194            owner: (int) ($row['owner'] ?? 1),
195            coOwners: $coOwners,
196            defaultForUsers: $defaultForUsers
197        );
198    }
199
200    /**
201     * Checks if this mailbox is configured as default for the given user ID.
202     *
203     * @param int $userId Target user ID.
204     * @return bool True if mailbox is marked default for user.
205     */
206    public function isDefaultForUser(int $userId): bool
207    {
208        return in_array($userId, $this->defaultForUsers, true);
209    }
210
211    /**
212     * Returns a clone of this mailbox with updated isDefault flag.
213     *
214     * @param bool $isDefault New default flag status for the requesting user context.
215     * @return self New instance with updated isDefault flag.
216     */
217    /**
218     * Creates a mutated clone of this mailbox with updated properties.
219     *
220     * @param array<string, mixed> $overrides Overridden properties.
221     */
222    private function copyWith(array $overrides): self
223    {
224        $props = get_object_vars($this);
225        foreach ($overrides as $key => $value) {
226            $props[$key] = $value;
227        }
228
229        return new self(...$props);
230    }
231
232    /**
233     * Returns a clone of this mailbox with updated isDefault flag.
234     *
235     * @param bool $isDefault New default flag status for the requesting user context.
236     * @return self New instance with updated isDefault flag.
237     */
238    public function withIsDefault(bool $isDefault): self
239    {
240        return $this->copyWith(['isDefault' => $isDefault]);
241    }
242
243    /**
244     * Returns a clone of this mailbox with a decrypted password for protocol connections.
245     *
246     * @param string $plainPassword Plaintext decrypted password.
247     * @return self New instance with decrypted password.
248     */
249    public function withDecryptedPassword(string $plainPassword): self
250    {
251        return $this->copyWith(['password' => $plainPassword]);
252    }
253}